summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStephane Delcroix <stephane@delcroix.org>2016-11-15 20:39:48 +0100
committerJason Smith <jason.smith@xamarin.com>2016-11-15 11:39:48 -0800
commita6bbed029c64d2d64b74eeb67e27a099abf70664 (patch)
tree551c3924c055e2d39592b3f1c726cca46924dd73
parent14e21dcebd4a706aaa5eed384b142957d84df002 (diff)
downloadxamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.tar.gz
xamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.tar.bz2
xamarin-forms-a6bbed029c64d2d64b74eeb67e27a099abf70664.zip
[XamlC] TypedBindings, some tests, a compiler, ... (#489)
-rw-r--r--Xamarin.Forms.Build.Tasks/DebugXamlCTask.cs68
-rw-r--r--Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs5
-rw-r--r--Xamarin.Forms.Build.Tasks/ILContext.cs5
-rw-r--r--Xamarin.Forms.Build.Tasks/Logger.cs76
-rw-r--r--Xamarin.Forms.Build.Tasks/MethodReferenceExtensions.cs18
-rw-r--r--Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs373
-rw-r--r--Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj2
-rw-r--r--Xamarin.Forms.Build.Tasks/XamlCTask.cs262
-rw-r--r--Xamarin.Forms.Build.Tasks/XamlTask.cs96
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla33870.cs2
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla34720.cs21
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla36009.cs2
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1400.cs6
-rw-r--r--Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1762.cs4
-rw-r--r--Xamarin.Forms.Controls/ControlGalleryPages/ListRefresh.cs4
-rw-r--r--Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs546
-rw-r--r--Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs701
-rw-r--r--Xamarin.Forms.Core.UnitTests/MockViewModel.cs22
-rw-r--r--Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs1526
-rw-r--r--Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj1
-rw-r--r--Xamarin.Forms.Core/BindableObject.cs34
-rw-r--r--Xamarin.Forms.Core/BindableObjectExtensions.cs1
-rw-r--r--Xamarin.Forms.Core/Binding.cs2
-rw-r--r--Xamarin.Forms.Core/BindingBase.cs8
-rw-r--r--Xamarin.Forms.Core/BindingBaseExtensions.cs13
-rw-r--r--Xamarin.Forms.Core/BindingExpression.cs63
-rw-r--r--Xamarin.Forms.Core/TypedBinding.cs291
-rw-r--r--Xamarin.Forms.Core/WeakReferenceExtensions.cs4
-rw-r--r--Xamarin.Forms.Core/Xamarin.Forms.Core.csproj1
-rw-r--r--Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml21
-rw-r--r--Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml.cs130
-rw-r--r--Xamarin.Forms.Xaml.UnitTests/MockCompiler.cs2
-rw-r--r--Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj16
-rw-r--r--Xamarin.Forms.Xaml.Xamlc/Xamlc.cs14
-rw-r--r--Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs3
-rw-r--r--Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs3
-rw-r--r--Xamarin.Forms.Xaml/MarkupExtensions/BindingExtension.cs14
-rw-r--r--Xamarin.Forms.Xaml/XamlCompilationAttribute.cs2
-rw-r--r--Xamarin.Forms.Xaml/XamlNode.cs5
-rw-r--r--Xamarin.Forms.Xaml/XamlParser.cs71
-rw-r--r--Xamarin.Forms.Xaml/XmlName.cs1
-rw-r--r--docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBindingBase.xml66
-rw-r--r--docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBinding`2.xml44
-rw-r--r--docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObject.xml16
-rw-r--r--docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObjectExtensions.xml5
-rw-r--r--docs/Xamarin.Forms.Core/Xamarin.Forms/Binding.xml5
-rw-r--r--docs/Xamarin.Forms.Core/index.xml2
-rw-r--r--docs/Xamarin.Forms.Xaml/Xamarin.Forms.Xaml/BindingExtension.xml16
48 files changed, 3545 insertions, 1048 deletions
diff --git a/Xamarin.Forms.Build.Tasks/DebugXamlCTask.cs b/Xamarin.Forms.Build.Tasks/DebugXamlCTask.cs
index c4744cc2..a8ef874a 100644
--- a/Xamarin.Forms.Build.Tasks/DebugXamlCTask.cs
+++ b/Xamarin.Forms.Build.Tasks/DebugXamlCTask.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
@@ -7,21 +8,20 @@ using Mono.Cecil.Rocks;
namespace Xamarin.Forms.Build.Tasks
{
- public class DebugXamlCTask : XamlCTask
+ public class DebugXamlCTask : XamlTask
{
- public override bool Execute()
+ public override bool Execute(IList<Exception> thrownExceptions)
{
- InMsBuild = true;
- Verbosity = Int32.MaxValue;
- LogLine(1, "Preparing debug code for xamlc");
- LogLine(1, "\nAssembly: {0}", Assembly);
+ Logger = Logger ?? new Logger(null, Verbosity);
+ Logger.LogLine(1, "Preparing debug code for xamlc");
+ Logger.LogLine(1, "\nAssembly: {0}", Assembly);
var resolver = new DefaultAssemblyResolver();
if (!string.IsNullOrEmpty(DependencyPaths))
{
foreach (var dep in DependencyPaths.Split(';'))
{
- LogLine(3, "Adding searchpath {0}", dep);
+ Logger.LogLine(3, "Adding searchpath {0}", dep);
resolver.AddSearchDirectory(dep);
}
}
@@ -31,7 +31,7 @@ namespace Xamarin.Forms.Build.Tasks
foreach (var p in paths)
{
var searchpath = Path.GetDirectoryName(p);
- LogLine(3, "Adding searchpath {0}", searchpath);
+ Logger.LogLine(3, "Adding searchpath {0}", searchpath);
resolver.AddSearchDirectory(searchpath);
// LogLine (3, "Referencing {0}", p);
// resolver.AddAssembly (p);
@@ -45,58 +45,58 @@ namespace Xamarin.Forms.Build.Tasks
foreach (var module in assemblyDefinition.Modules)
{
- LogLine(2, " Module: {0}", module.Name);
+ Logger.LogLine(2, " Module: {0}", module.Name);
foreach (var resource in module.Resources.OfType<EmbeddedResource>())
{
- LogString(2, " Resource: {0}... ", resource.Name);
+ Logger.LogString(2, " Resource: {0}... ", resource.Name);
string classname;
if (!resource.IsXaml(out classname))
{
- LogLine(2, "skipped.");
+ Logger.LogLine(2, "skipped.");
continue;
}
TypeDefinition typeDef = module.GetType(classname);
if (typeDef == null)
{
- LogLine(2, "no type found... skipped.");
+ Logger.LogLine(2, "no type found... skipped.");
continue;
}
var initComp = typeDef.Methods.FirstOrDefault(md => md.Name == "InitializeComponent");
if (initComp == null)
{
- LogLine(2, "no InitializeComponent found... skipped.");
+ Logger.LogLine(2, "no InitializeComponent found... skipped.");
continue;
}
var initCompRuntime = typeDef.Methods.FirstOrDefault(md => md.Name == "__InitComponentRuntime");
if (initCompRuntime == null) {
- LogLine(2, "no __InitComponentRuntime found... duplicating.");
+ Logger.LogLine(2, "no __InitComponentRuntime found... duplicating.");
initCompRuntime = DuplicateMethodDef(typeDef, initComp, "__InitComponentRuntime");
}
- // IL_0000: ldarg.0
- // IL_0001: callvirt instance void class [Xamarin.Forms.Core]Xamarin.Forms.ContentPage::'.ctor'()
- //
- // IL_0006: nop
- // IL_0007: ldarg.1
- // IL_0008: brfalse IL_0018
- //
- // IL_000d: ldarg.0
- // IL_000e: callvirt instance void class Xamarin.Forms.Xaml.XamlcTests.MyPage::InitializeComponent()
- // IL_0013: br IL_001e
- //
- // IL_0018: ldarg.0
- // IL_0019: callvirt instance void class Xamarin.Forms.Xaml.XamlcTests.MyPage::__InitComponentRuntime()
- // IL_001e: ret
+// IL_0000: ldarg.0
+// IL_0001: callvirt instance void class [Xamarin.Forms.Core]Xamarin.Forms.ContentPage::'.ctor'()
+//
+// IL_0006: nop
+// IL_0007: ldarg.1
+// IL_0008: brfalse IL_0018
+//
+// IL_000d: ldarg.0
+// IL_000e: callvirt instance void class Xamarin.Forms.Xaml.XamlcTests.MyPage::InitializeComponent()
+// IL_0013: br IL_001e
+//
+// IL_0018: ldarg.0
+// IL_0019: callvirt instance void class Xamarin.Forms.Xaml.XamlcTests.MyPage::__InitComponentRuntime()
+// IL_001e: ret
var altCtor =
typeDef.Methods.Where(
md => md.IsConstructor && md.Parameters.Count == 1 && md.Parameters[0].ParameterType == module.TypeSystem.Boolean)
.FirstOrDefault();
if (altCtor != null)
- LogString(2, " Replacing body of {0}.{0} (bool {1}) ... ", typeDef.Name, altCtor.Parameters[0].Name);
+ Logger.LogString(2, " Replacing body of {0}.{0} (bool {1}) ... ", typeDef.Name, altCtor.Parameters[0].Name);
else
{
- LogString(2, " Adding {0}.{0} (bool useCompiledXaml) ... ", typeDef.Name);
+ Logger.LogString(2, " Adding {0}.{0} (bool useCompiledXaml) ... ", typeDef.Name);
altCtor = new MethodDefinition(".ctor",
MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName, module.TypeSystem.Void);
@@ -127,17 +127,17 @@ namespace Xamarin.Forms.Build.Tasks
altCtor.Body = body;
if (!typeDef.Methods.Contains(altCtor))
typeDef.Methods.Add(altCtor);
- LogLine(2, "done.");
+ Logger.LogLine(2, "done.");
}
- LogLine(2, "");
+ Logger.LogLine(2, "");
}
- LogString(1, "Writing the assembly... ");
+ Logger.LogString(1, "Writing the assembly... ");
assemblyDefinition.Write(Assembly, new WriterParameters
{
WriteSymbols = DebugSymbols
});
- LogLine(1, "done.");
+ Logger.LogLine(1, "done.");
return true;
}
diff --git a/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs b/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs
index 26e241ab..e647f6c2 100644
--- a/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs
+++ b/Xamarin.Forms.Build.Tasks/ExpandMarkupsVisitor.cs
@@ -13,7 +13,8 @@ namespace Xamarin.Forms.Build.Tasks
XmlName.xKey,
XmlName.xTypeArguments,
XmlName.xFactoryMethod,
- XmlName.xName
+ XmlName.xName,
+ XmlName.xDataType
};
public ExpandMarkupsVisitor(ILContext context)
@@ -165,7 +166,7 @@ namespace Xamarin.Forms.Build.Tasks
try
{
type = new XmlType(namespaceuri, name + "Extension", null);
- type.GetTypeReference(contextProvider.Context.Body.Method.Module, null);
+ type.GetTypeReference(contextProvider.Context.Module, null);
}
catch (XamlParseException)
{
diff --git a/Xamarin.Forms.Build.Tasks/ILContext.cs b/Xamarin.Forms.Build.Tasks/ILContext.cs
index 14ade391..8f36227a 100644
--- a/Xamarin.Forms.Build.Tasks/ILContext.cs
+++ b/Xamarin.Forms.Build.Tasks/ILContext.cs
@@ -7,7 +7,7 @@ namespace Xamarin.Forms.Build.Tasks
{
class ILContext
{
- public ILContext(ILProcessor il, MethodBody body, FieldDefinition parentContextValues = null)
+ public ILContext(ILProcessor il, MethodBody body, ModuleDefinition module, FieldDefinition parentContextValues = null)
{
IL = il;
Body = body;
@@ -16,6 +16,7 @@ namespace Xamarin.Forms.Build.Tasks
Scopes = new Dictionary<INode, VariableDefinition>();
TypeExtensions = new Dictionary<INode, TypeReference>();
ParentContextValues = parentContextValues;
+ Module = module;
}
public Dictionary<IValueNode, object> Values { get; private set; }
@@ -33,5 +34,7 @@ namespace Xamarin.Forms.Build.Tasks
public ILProcessor IL { get; private set; }
public MethodBody Body { get; private set; }
+
+ public ModuleDefinition Module { get; private set; }
}
} \ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/Logger.cs b/Xamarin.Forms.Build.Tasks/Logger.cs
new file mode 100644
index 00000000..b706e610
--- /dev/null
+++ b/Xamarin.Forms.Build.Tasks/Logger.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Xml;
+using Microsoft.Build.Utilities;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Build.Tasks
+{
+ public class Logger {
+ public TaskLoggingHelper Helper { get; }
+ public int Verbosity { get; }
+
+ public Logger(TaskLoggingHelper helper, int verbosity)
+ {
+ Verbosity = verbosity;
+ Helper = helper;
+ }
+
+ string buffer = "";
+
+ public void LogException(string subcategory, string errorCode, string helpKeyword, string file, Exception e)
+ {
+ var xpe = e as XamlParseException;
+ var xe = e as XmlException;
+ if (xpe != null)
+ LogError(subcategory, errorCode, helpKeyword, file, xpe.XmlInfo.LineNumber, xpe.XmlInfo.LinePosition, 0, 0, xpe.Message, xpe.HelpLink, xpe.Source);
+ else if (xe != null)
+ LogError(subcategory, errorCode, helpKeyword, file, xe.LineNumber, xe.LinePosition, 0, 0, xe.Message, xe.HelpLink, xe.Source);
+ else
+ LogError(subcategory, errorCode, helpKeyword, file, 0, 0, 0, 0, e.Message, e.HelpLink, e.Source);
+ }
+
+ public void LogError(string subcategory, string errorCode, string helpKeyword, string file, int lineNumber,
+ int columnNumber, int endLineNumber, int endColumnNumber, string message, params object [] messageArgs)
+ {
+ if (!string.IsNullOrEmpty(buffer))
+ LogLine(-1, null, null);
+ if (Helper != null) {
+ Helper.LogError(subcategory, errorCode, helpKeyword, file, lineNumber, columnNumber, endLineNumber,
+ endColumnNumber, message, messageArgs);
+ } else
+ Console.Error.WriteLine($"{file} ({lineNumber}:{columnNumber}) : {message}");
+ }
+
+ public void LogLine(int level, string format, params object [] arg)
+ {
+ if (!string.IsNullOrEmpty(buffer)) {
+ format = buffer + format;
+ buffer = "";
+ }
+
+ if (level < 0) {
+ if (Helper != null)
+ Helper.LogError(format, arg);
+ else
+ Console.Error.WriteLine(format, arg);
+ } else if (level <= Verbosity) {
+ if (Helper != null)
+ Helper.LogMessage(format, arg);
+ else
+ Console.WriteLine(format, arg);
+ }
+ }
+
+ public void LogString(int level, string format, params object [] arg)
+ {
+ if (level <= 0)
+ Console.Error.Write(format, arg);
+ else if (level <= Verbosity) {
+ if (Helper != null)
+ buffer += String.Format(format, arg);
+ else
+ Console.Write(format, arg);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/MethodReferenceExtensions.cs b/Xamarin.Forms.Build.Tasks/MethodReferenceExtensions.cs
index 3baa230c..13b4aafc 100644
--- a/Xamarin.Forms.Build.Tasks/MethodReferenceExtensions.cs
+++ b/Xamarin.Forms.Build.Tasks/MethodReferenceExtensions.cs
@@ -40,5 +40,23 @@ namespace Xamarin.Forms.Build.Tasks
self.Parameters[i].ParameterType = module.Import(self.Parameters[i].ParameterType);
}
}
+
+ public static MethodReference MakeGeneric(this MethodReference self, TypeReference declaringType, params TypeReference [] arguments)
+ {
+ var reference = new MethodReference(self.Name, self.ReturnType) {
+ DeclaringType = declaringType,
+ HasThis = self.HasThis,
+ ExplicitThis = self.ExplicitThis,
+ CallingConvention = self.CallingConvention,
+ };
+
+ foreach (var parameter in self.Parameters)
+ reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
+
+ foreach (var generic_parameter in self.GenericParameters)
+ reference.GenericParameters.Add(new GenericParameter(generic_parameter.Name, reference));
+
+ return reference;
+ }
}
} \ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs b/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs
index f481cbee..8b4bf7ad 100644
--- a/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs
+++ b/Xamarin.Forms.Build.Tasks/SetPropertiesVisitor.cs
@@ -16,6 +16,7 @@ namespace Xamarin.Forms.Build.Tasks
class SetPropertiesVisitor : IXamlNodeVisitor
{
static int dtcount;
+ static int typedBindingCount;
static readonly IList<XmlName> skips = new List<XmlName>
{
@@ -23,7 +24,8 @@ namespace Xamarin.Forms.Build.Tasks
XmlName.xTypeArguments,
XmlName.xArguments,
XmlName.xFactoryMethod,
- XmlName.xName
+ XmlName.xName,
+ XmlName.xDataType
};
public SetPropertiesVisitor(ILContext context, bool stopOnResourceDictionary = false)
@@ -271,6 +273,10 @@ namespace Xamarin.Forms.Build.Tasks
else if (vardefref.VariableDefinition.VariableType.ImplementsGenericInterface("Xamarin.Forms.Xaml.IMarkupExtension`1",
out markupExtension, out genericArguments))
{
+ if (vardefref.VariableDefinition.VariableType.FullName == "Xamarin.Forms.Xaml.BindingExtension")
+ foreach (var instruction in CompileBindingPath(node, context, vardefref.VariableDefinition))
+ yield return instruction;
+
var markExt = markupExtension.Resolve();
var provideValueInfo = markExt.Methods.First(md => md.Name == "ProvideValue");
var provideValue = module.Import(provideValueInfo);
@@ -312,6 +318,359 @@ namespace Xamarin.Forms.Build.Tasks
}
}
+ //Once we get compiled IValueProvider, this will move to the BindingExpression
+ static IEnumerable<Instruction> CompileBindingPath(ElementNode node, ILContext context, VariableDefinition bindingExt)
+ {
+ //TODO implement handlers[]
+ //TODO support casting operators
+
+ INode pathNode;
+ if (!node.Properties.TryGetValue(new XmlName("", "Path"), out pathNode) && node.CollectionItems.Any())
+ pathNode = node.CollectionItems [0];
+ var path = (pathNode as ValueNode)?.Value as string;
+
+ INode dataTypeNode = null;
+ IElementNode n = node;
+ while (n != null) {
+ if (n.Properties.TryGetValue(XmlName.xDataType, out dataTypeNode))
+ break;
+ n = n.Parent as IElementNode;
+ }
+ var dataType = (dataTypeNode as ValueNode)?.Value as string;
+ if (dataType == null)
+ yield break; //throw
+
+ var namespaceuri = dataType.Contains(":") ? node.NamespaceResolver.LookupNamespace(dataType.Split(':') [0].Trim()) : "";
+ var dtXType = new XmlType(namespaceuri, dataType, null);
+
+ var tSourceRef = dtXType.GetTypeReference(context.Module, (IXmlLineInfo)node);
+ if (tSourceRef == null)
+ yield break; //throw
+
+ var properties = ParsePath(path, tSourceRef, node as IXmlLineInfo, context.Module);
+ var tPropertyRef = properties != null && properties.Any() ? properties.Last().Item1.PropertyType : tSourceRef;
+
+ var funcRef = context.Module.Import(context.Module.Import(typeof(Func<,>)).MakeGenericInstanceType(new [] { tSourceRef, tPropertyRef }));
+ var actionRef = context.Module.Import(context.Module.Import(typeof(Action<,>)).MakeGenericInstanceType(new [] { tSourceRef, tPropertyRef }));
+ var funcObjRef = context.Module.Import(context.Module.Import(typeof(Func<,>)).MakeGenericInstanceType(new [] { tSourceRef, context.Module.TypeSystem.Object }));
+ var tupleRef = context.Module.Import(context.Module.Import(typeof(Tuple<,>)).MakeGenericInstanceType(new [] { funcObjRef, context.Module.TypeSystem.String}));
+ var typedBindingRef = context.Module.Import(context.Module.Import(typeof(TypedBinding<,>)).MakeGenericInstanceType(new [] { tSourceRef, tPropertyRef}));
+
+ TypeReference _;
+ var ctorInfo = context.Module.Import(typedBindingRef.Resolve().Methods.FirstOrDefault(md => md.IsConstructor && !md.IsStatic && md.Parameters.Count == 3 ));
+ var ctorinforef = ctorInfo.MakeGeneric(typedBindingRef, funcRef, actionRef, tupleRef);
+ var setTypedBinding = context.Module.Import(typeof(BindingExtension)).GetProperty(pd => pd.Name == "TypedBinding", out _).SetMethod;
+
+ yield return Instruction.Create(OpCodes.Ldloc, bindingExt);
+ foreach (var instruction in CompiledBindingGetGetter(tSourceRef, tPropertyRef, properties, node, context))
+ yield return instruction;
+ foreach (var instruction in CompiledBindingGetSetter(tSourceRef, tPropertyRef, properties, node, context))
+ yield return instruction;
+ foreach (var instruction in CompiledBindingGetHandlers(tSourceRef, tPropertyRef, properties, node, context))
+ yield return instruction;
+ yield return Instruction.Create(OpCodes.Newobj, context.Module.Import(ctorinforef));
+ yield return Instruction.Create(OpCodes.Callvirt, context.Module.Import(setTypedBinding));
+ }
+
+ static IList<Tuple<PropertyDefinition, string>> ParsePath(string path, TypeReference tSourceRef, IXmlLineInfo lineInfo, ModuleDefinition module)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return null;
+ path = path.Trim(' ', '.'); //trim leading or trailing dots
+ var parts = path.Split(new [] { '.' }, StringSplitOptions.RemoveEmptyEntries);
+ var properties = new List<Tuple<PropertyDefinition, string>>();
+
+ var previousPartTypeRef = tSourceRef;
+ TypeReference _;
+ foreach (var part in parts) {
+ var p = part;
+ string indexArg = null;
+ var lbIndex = p.IndexOf('[');
+ if (lbIndex != -1) {
+ var rbIndex = p.LastIndexOf(']');
+ if (rbIndex == -1)
+ throw new XamlParseException("Binding: Indexer did not contain closing bracket", lineInfo);
+
+ var argLength = rbIndex - lbIndex - 1;
+ if (argLength == 0)
+ throw new XamlParseException("Binding: Indexer did not contain arguments", lineInfo);
+
+ indexArg = p.Substring(lbIndex + 1, argLength).Trim();
+ if (indexArg.Length == 0)
+ throw new XamlParseException("Binding: Indexer did not contain arguments", lineInfo);
+
+ p = p.Substring(0, lbIndex);
+ p = p.Trim();
+ }
+
+ if (p.Length > 0) {
+ var property = previousPartTypeRef.GetProperty(pd => pd.Name == p && pd.GetMethod != null && pd.GetMethod.IsPublic, out _);
+ properties.Add(new Tuple<PropertyDefinition, string>(property,null));
+ previousPartTypeRef = property.PropertyType;
+ }
+ if (indexArg != null) {
+ var defaultMemberAttribute = previousPartTypeRef.GetCustomAttribute(module.Import(typeof(System.Reflection.DefaultMemberAttribute)));
+ var indexerName = defaultMemberAttribute?.ConstructorArguments?.FirstOrDefault().Value as string ?? "Item";
+ var indexer = previousPartTypeRef.GetProperty(pd => pd.Name == indexerName && pd.GetMethod != null && pd.GetMethod.IsPublic, out _);
+ properties.Add(new Tuple<PropertyDefinition, string>(indexer, indexArg));
+ if (indexer.PropertyType != module.TypeSystem.String && indexer.PropertyType != module.TypeSystem.Int32)
+ throw new XamlParseException($"Binding: Unsupported indexer index type: {indexer.PropertyType.FullName}", lineInfo);
+ previousPartTypeRef = indexer.PropertyType;
+ }
+ }
+ return properties;
+ }
+
+ static IEnumerable<Instruction> CompiledBindingGetGetter(TypeReference tSourceRef, TypeReference tPropertyRef, IList<Tuple<PropertyDefinition, string>> properties, ElementNode node, ILContext context)
+ {
+// .method private static hidebysig default string '<Main>m__0' (class ViewModel vm) cil managed
+// {
+// .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ...
+//
+// IL_0000: ldarg.0
+// IL_0001: callvirt instance class ViewModel class ViewModel::get_Model()
+// IL_0006: callvirt instance string class ViewModel::get_Text()
+// IL_0006: ret
+// }
+
+ var module = context.Module;
+ var compilerGeneratedCtor = module.Import(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)).GetMethods(md => md.IsConstructor, module).First().Item1;
+ var getter = new MethodDefinition($"<{context.Body.Method.Name}>typedBindingsM__{typedBindingCount++}",
+ MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static,
+ tPropertyRef) {
+ Parameters = {
+ new ParameterDefinition(tSourceRef)
+ },
+ CustomAttributes = {
+ new CustomAttribute (context.Module.Import(compilerGeneratedCtor))
+ }
+ };
+ var il = getter.Body.GetILProcessor();
+
+ il.Emit(OpCodes.Ldarg_0);
+ if (properties != null && properties.Count != 0) {
+ foreach (var propTuple in properties) {
+ var property = propTuple.Item1;
+ var indexerArg = propTuple.Item2;
+ if (indexerArg != null) {
+ if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.String)
+ il.Emit(OpCodes.Ldstr, indexerArg);
+ else if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.Int32) {
+ int index;
+ if (!int.TryParse(indexerArg, out index))
+ throw new XamlParseException($"Binding: {indexerArg} could not be parsed as an index for a {property.Name}", node as IXmlLineInfo);
+ il.Emit(OpCodes.Ldc_I4, index);
+ }
+ }
+ il.Emit(OpCodes.Callvirt, module.Import(property.GetMethod));
+ }
+ }
+
+ il.Emit(OpCodes.Ret);
+
+ context.Body.Method.DeclaringType.Methods.Add(getter);
+
+ var funcRef = module.Import(typeof(Func<,>));
+ funcRef = module.Import(funcRef.MakeGenericInstanceType(new [] { tSourceRef, tPropertyRef }));
+ var funcCtor = module.Import(funcRef.Resolve().GetConstructors().First());
+ funcCtor = funcCtor.MakeGeneric(funcRef, new [] { tSourceRef, tPropertyRef });
+
+// IL_0007: ldnull
+// IL_0008: ldftn string class Test::'<Main>m__0'(class ViewModel)
+// IL_000e: newobj instance void class [mscorlib]System.Func`2<class ViewModel, string>::'.ctor'(object, native int)
+
+ yield return Instruction.Create(OpCodes.Ldnull);
+ yield return Instruction.Create(OpCodes.Ldftn, getter);
+ yield return Instruction.Create(OpCodes.Newobj, module.Import(funcCtor));
+ }
+
+ static IEnumerable<Instruction> CompiledBindingGetSetter(TypeReference tSourceRef, TypeReference tPropertyRef, IList<Tuple<PropertyDefinition, string>> properties, ElementNode node, ILContext context)
+ {
+ if (properties == null || properties.Count == 0) {
+ yield return Instruction.Create(OpCodes.Ldnull);
+ yield break;
+ }
+
+ // .method private static hidebysig default void '<Main>m__1' (class ViewModel vm, string s) cil managed
+ // {
+ // .custom instance void class [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....
+ //
+ // IL_0000: ldarg.0
+ // IL_0001: callvirt instance class ViewModel class ViewModel::get_Model()
+ // IL_0006: ldarg.1
+ // IL_0007: callvirt instance void class ViewModel::set_Text(string)
+ // IL_000c: ret
+ // }
+
+ var module = context.Module;
+ var compilerGeneratedCtor = module.Import(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)).GetMethods(md => md.IsConstructor, module).First().Item1;
+ var setter = new MethodDefinition($"<{context.Body.Method.Name}>typedBindingsM__{typedBindingCount++}",
+ MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static,
+ module.TypeSystem.Void) {
+ Parameters = {
+ new ParameterDefinition(tSourceRef),
+ new ParameterDefinition(tPropertyRef)
+ },
+ CustomAttributes = {
+ new CustomAttribute (module.Import(compilerGeneratedCtor))
+ }
+ };
+
+ var il = setter.Body.GetILProcessor();
+ var lastProperty = properties.LastOrDefault();
+ var setterRef = lastProperty?.Item1.SetMethod;
+ if (setterRef == null) {
+ yield return Instruction.Create(OpCodes.Ldnull); //throw or not ?
+ yield break;
+ }
+
+ il.Emit(OpCodes.Ldarg_0);
+ for (int i = 0; i < properties.Count - 1; i++) {
+ var property = properties[i].Item1;
+ var indexerArg = properties[i].Item2;
+ if (indexerArg != null) {
+ if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.String)
+ il.Emit(OpCodes.Ldstr, indexerArg);
+ else if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.Int32) {
+ int index;
+ if (!int.TryParse(indexerArg, out index))
+ throw new XamlParseException($"Binding: {indexerArg} could not be parsed as an index for a {property.Name}", node as IXmlLineInfo);
+ il.Emit(OpCodes.Ldc_I4, index);
+ }
+ }
+ il.Emit(OpCodes.Callvirt, module.Import(property.GetMethod));
+ }
+
+ var indexer = properties.Last().Item2;
+ if (indexer != null) {
+ if (lastProperty.Item1.GetMethod.Parameters [0].ParameterType == module.TypeSystem.String)
+ il.Emit(OpCodes.Ldstr, indexer);
+ else if (lastProperty.Item1.GetMethod.Parameters [0].ParameterType == module.TypeSystem.Int32) {
+ int index;
+ if (!int.TryParse(indexer, out index))
+ throw new XamlParseException($"Binding: {indexer} could not be parsed as an index for a {lastProperty.Item1.Name}", node as IXmlLineInfo);
+ il.Emit(OpCodes.Ldc_I4, index);
+ }
+ }
+ il.Emit(OpCodes.Ldarg_1);
+ il.Emit(OpCodes.Callvirt, module.Import(setterRef));
+ il.Emit(OpCodes.Ret);
+
+ context.Body.Method.DeclaringType.Methods.Add(setter);
+
+ var actionRef = module.Import(typeof(Action<,>));
+ actionRef = module.Import(actionRef.MakeGenericInstanceType(new [] { tSourceRef, tPropertyRef }));
+ var actionCtor = module.Import(actionRef.Resolve().GetConstructors().First());
+ actionCtor = actionCtor.MakeGeneric(actionRef, new [] { tSourceRef, tPropertyRef });
+
+// IL_0024: ldnull
+// IL_0025: ldftn void class Test::'<Main>m__1'(class ViewModel, string)
+// IL_002b: newobj instance void class [mscorlib]System.Action`2<class ViewModel, string>::'.ctor'(object, native int)
+ yield return Instruction.Create(OpCodes.Ldnull);
+ yield return Instruction.Create(OpCodes.Ldftn, setter);
+ yield return Instruction.Create(OpCodes.Newobj, module.Import(actionCtor));
+ }
+
+ static IEnumerable<Instruction> CompiledBindingGetHandlers(TypeReference tSourceRef, TypeReference tPropertyRef, IList<Tuple<PropertyDefinition, string>> properties, ElementNode node, ILContext context)
+ {
+// .method private static hidebysig default object '<Main>m__2'(class ViewModel vm) cil managed {
+// .custom instance void class [mscorlib] System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....
+// IL_0000: ldarg.0
+// IL_0001: ret
+// } // end of method Test::<Main>m__2
+
+// .method private static hidebysig default object '<Main>m__3' (class ViewModel vm) cil managed {
+// .custom instance void class [mscorlib] System.Runtime.CompilerServices.CompilerGeneratedAttribute::'.ctor'() = (01 00 00 00 ) // ....
+// IL_0000: ldarg.0
+// IL_0001: callvirt instance class ViewModel class ViewModel::get_Model()
+// IL_0006: ret
+// }
+
+ var module = context.Module;
+ var compilerGeneratedCtor = module.Import(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)).GetMethods(md => md.IsConstructor, module).First().Item1;
+
+ var partGetters = new List<MethodDefinition>();
+ if (properties == null || properties.Count == 0) {
+ yield return Instruction.Create(OpCodes.Ldnull);
+ yield break;
+ }
+
+ for (int i = 0; i < properties.Count; i++) {
+ var tuple = properties [i];
+ var partGetter = new MethodDefinition($"<{context.Body.Method.Name}>typedBindingsM__{typedBindingCount++}", MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, tPropertyRef) {
+ Parameters = {
+ new ParameterDefinition(tSourceRef)
+ },
+ CustomAttributes = {
+ new CustomAttribute (context.Module.Import(compilerGeneratedCtor))
+ }
+ };
+ var il = partGetter.Body.GetILProcessor();
+ il.Emit(OpCodes.Ldarg_0);
+ for (int j = 0; j < i; j++) {
+ var propTuple = properties [j];
+ var property = propTuple.Item1;
+ var indexerArg = propTuple.Item2;
+ if (indexerArg != null) {
+ if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.String)
+ il.Emit(OpCodes.Ldstr, indexerArg);
+ else if (property.GetMethod.Parameters [0].ParameterType == module.TypeSystem.Int32) {
+ int index;
+ if (!int.TryParse(indexerArg, out index))
+ throw new XamlParseException($"Binding: {indexerArg} could not be parsed as an index for a {property.Name}", node as IXmlLineInfo);
+ il.Emit(OpCodes.Ldc_I4, index);
+ }
+ }
+ il.Emit(OpCodes.Callvirt, module.Import(property.GetMethod));
+ }
+ il.Emit(OpCodes.Ret);
+ context.Body.Method.DeclaringType.Methods.Add(partGetter);
+ partGetters.Add(partGetter);
+ }
+
+ var funcObjRef = context.Module.Import(module.Import(typeof(Func<,>)).MakeGenericInstanceType(new [] { tSourceRef, module.TypeSystem.Object }));
+ var tupleRef = context.Module.Import(module.Import(typeof(Tuple<,>)).MakeGenericInstanceType(new [] { funcObjRef, module.TypeSystem.String }));
+ var funcCtor = module.Import(funcObjRef.Resolve().GetConstructors().First());
+ funcCtor = funcCtor.MakeGeneric(funcObjRef, new [] { tSourceRef, module.TypeSystem.Object });
+ var tupleCtor = module.Import(tupleRef.Resolve().GetConstructors().First());
+ tupleCtor = tupleCtor.MakeGeneric(tupleRef, new [] { funcObjRef, module.TypeSystem.String});
+
+// IL_003a: ldc.i4.2
+// IL_003b: newarr class [mscorlib] System.Tuple`2<class [mscorlib]System.Func`2<class ViewModel,object>,string>
+
+// IL_0040: dup
+// IL_0041: ldc.i4.0
+// IL_0049: ldnull
+// IL_004a: ldftn object class Test::'<Main>m__2'(class ViewModel)
+// IL_0050: newobj instance void class [mscorlib]System.Func`2<class ViewModel, object>::'.ctor'(object, native int)
+// IL_005f: ldstr "Model"
+// IL_0064: newobj instance void class [mscorlib]System.Tuple`2<class [mscorlib]System.Func`2<class ViewModel, object>, string>::'.ctor'(!0, !1)
+// IL_0069: stelem.ref
+
+// IL_006a: dup
+// IL_006b: ldc.i4.1
+// IL_0073: ldnull
+// IL_0074: ldftn object class Test::'<Main>m__3'(class ViewModel)
+// IL_007a: newobj instance void class [mscorlib]System.Func`2<class ViewModel, object>::'.ctor'(object, native int)
+// IL_0089: ldstr "Text"
+// IL_008e: newobj instance void class [mscorlib]System.Tuple`2<class [mscorlib]System.Func`2<class ViewModel, object>, string>::'.ctor'(!0, !1)
+// IL_0093: stelem.ref
+
+ yield return Instruction.Create(OpCodes.Ldc_I4, properties.Count);
+ yield return Instruction.Create(OpCodes.Newarr, tupleRef);
+
+ for (var i = 0; i < properties.Count; i++) {
+ yield return Instruction.Create(OpCodes.Dup);
+ yield return Instruction.Create(OpCodes.Ldc_I4, i);
+ yield return Instruction.Create(OpCodes.Ldnull);
+ yield return Instruction.Create(OpCodes.Ldftn, partGetters [i]);
+ yield return Instruction.Create(OpCodes.Newobj, module.Import(funcCtor));
+ yield return Instruction.Create(OpCodes.Ldstr, properties [i].Item1.Name);
+ yield return Instruction.Create(OpCodes.Newobj, module.Import(tupleCtor));
+ yield return Instruction.Create(OpCodes.Stelem_Ref);
+ }
+ }
+
public static IEnumerable<Instruction> SetPropertyValue(VariableDefinition parent, XmlName propertyName, INode valueNode, ILContext context, IXmlLineInfo iXmlLineInfo)
{
var module = context.Body.Method.Module;
@@ -685,7 +1044,9 @@ namespace Xamarin.Forms.Build.Tasks
// .class nested private auto ansi sealed beforefieldinit '<Main>c__AnonStorey0'
// extends [mscorlib]System.Object
- var module = parentContext.Body.Method.Module;
+
+ var module = parentContext.Module;
+ var compilerGeneratedCtor = module.Import(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute)).GetMethods(md => md.IsConstructor, module).First().Item1;
var anonType = new TypeDefinition(
null,
"<" + parentContext.Body.Method.Name + ">_anonXamlCDataTemplate_" + dtcount++,
@@ -693,7 +1054,10 @@ namespace Xamarin.Forms.Build.Tasks
TypeAttributes.Sealed |
TypeAttributes.NestedPrivate)
{
- BaseType = module.TypeSystem.Object
+ BaseType = module.TypeSystem.Object,
+ CustomAttributes = {
+ new CustomAttribute (module.Import(compilerGeneratedCtor))
+ }
};
parentContext.Body.Method.DeclaringType.NestedTypes.Add(anonType);
@@ -721,7 +1085,7 @@ namespace Xamarin.Forms.Build.Tasks
//Fill the loadTemplate Body
var templateIl = loadTemplate.Body.GetILProcessor();
templateIl.Emit(OpCodes.Nop);
- var templateContext = new ILContext(templateIl, loadTemplate.Body, parentValues)
+ var templateContext = new ILContext(templateIl, loadTemplate.Body, module, parentValues)
{
Root = root
};
@@ -730,6 +1094,7 @@ namespace Xamarin.Forms.Build.Tasks
node.Accept(new SetFieldVisitor(templateContext), null);
node.Accept(new SetResourcesVisitor(templateContext), null);
node.Accept(new SetPropertiesVisitor(templateContext), null);
+
templateIl.Emit(OpCodes.Ldloc, templateContext.Variables[node]);
templateIl.Emit(OpCodes.Ret);
diff --git a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
index b7250038..6c820c67 100644
--- a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
+++ b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj
@@ -98,6 +98,8 @@
<Compile Include="CompiledConverters\ICompiledTypeConverter.cs" />
<Compile Include="CompiledConverters\LayoutOptionsConverter.cs" />
<Compile Include="CompiledConverters\RectangleTypeConverter.cs" />
+ <Compile Include="Logger.cs" />
+ <Compile Include="XamlTask.cs" />
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Target Name="AfterBuild">
diff --git a/Xamarin.Forms.Build.Tasks/XamlCTask.cs b/Xamarin.Forms.Build.Tasks/XamlCTask.cs
index 206fc452..689a6a59 100644
--- a/Xamarin.Forms.Build.Tasks/XamlCTask.cs
+++ b/Xamarin.Forms.Build.Tasks/XamlCTask.cs
@@ -1,13 +1,9 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Xml;
using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.Ast;
-using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
@@ -15,134 +11,31 @@ using Xamarin.Forms.Xaml;
namespace Xamarin.Forms.Build.Tasks
{
- public class XamlCTask : AppDomainIsolatedTask
+ public class XamlCTask : XamlTask
{
- string buffer = "";
-
bool hasCompiledXamlResources;
-
- [Required]
- public string Assembly { get; set; }
-
- public string DependencyPaths { get; set; }
-
- public string ReferencePath { get; set; }
-
- public int Verbosity { get; set; }
-
public bool KeepXamlResources { get; set; }
-
public bool OptimizeIL { get; set; }
-
- public bool DebugSymbols { get; set; }
-
public bool OutputGeneratedILAsCode { get; set; }
- protected bool InMsBuild { get; set; }
-
internal string Type { get; set; }
- public override bool Execute()
- {
- InMsBuild = true;
- return Compile();
- }
-
- protected void LogException(string subcategory, string errorCode, string helpKeyword, string file, Exception e)
- {
- var xpe = e as XamlParseException;
- var xe = e as XmlException;
- if (xpe != null)
- LogError(subcategory, errorCode, helpKeyword, file, xpe.XmlInfo.LineNumber, xpe.XmlInfo.LinePosition, 0, 0, xpe.Message, xpe.HelpLink, xpe.Source);
- else if (xe != null)
- LogError(subcategory, errorCode, helpKeyword, file, xe.LineNumber, xe.LinePosition, 0, 0, xe.Message, xe.HelpLink, xe.Source);
- else
- LogError(subcategory, errorCode, helpKeyword, file, 0, 0, 0, 0, e.Message, e.HelpLink, e.Source);
- }
-
- protected void LogError(string subcategory, string errorCode, string helpKeyword, string file, int lineNumber,
- int columnNumber, int endLineNumber, int endColumnNumber, string message, params object[] messageArgs)
- {
- if (!string.IsNullOrEmpty(buffer))
- LogLine(-1, null, null);
- if (InMsBuild)
- {
- base.Log.LogError(subcategory, errorCode, helpKeyword, file, lineNumber, columnNumber, endLineNumber,
- endColumnNumber, message, messageArgs);
- }
- else
- Console.Error.WriteLine("{0} ({1}:{2}) : {3}", file, lineNumber, columnNumber, message);
- }
-
- protected void LogLine(int level, string format, params object[] arg)
- {
- if (!string.IsNullOrEmpty(buffer))
- {
- format = buffer + format;
- buffer = "";
- }
-
- if (level < 0)
- {
- if (InMsBuild)
- base.Log.LogError(format, arg);
- else
- Console.Error.WriteLine(format, arg);
- }
- else if (level <= Verbosity)
- {
- if (InMsBuild)
- base.Log.LogMessage(format, arg);
- else
- Console.WriteLine(format, arg);
- }
- }
-
- protected void LogString(int level, string format, params object[] arg)
- {
- if (level <= 0)
- Console.Error.Write(format, arg);
- else if (level <= Verbosity)
- {
- if (InMsBuild)
- buffer += String.Format(format, arg);
- else
- Console.Write(format, arg);
- }
- }
-
- public static void Compile(string assemblyFileName, int verbosity = 0, bool keep = false, bool optimize = false,
- string dependencyPaths = null, string referencePath = null, bool outputCSharp = false)
+ public override bool Execute(IList<Exception> thrownExceptions)
{
- var xamlc = new XamlCTask
- {
- Assembly = assemblyFileName,
- Verbosity = verbosity,
- KeepXamlResources = keep,
- OptimizeIL = optimize,
- InMsBuild = false,
- DependencyPaths = dependencyPaths,
- ReferencePath = referencePath,
- OutputGeneratedILAsCode = outputCSharp,
- };
- xamlc.Compile();
- }
-
- public bool Compile(IList<Exception> thrownExceptions = null)
- {
- LogLine(1, "Compiling Xaml");
- LogLine(1, "\nAssembly: {0}", Assembly);
+ Logger = Logger ?? new Logger(null, Verbosity);
+ Logger.LogLine(1, "Compiling Xaml");
+ Logger.LogLine(1, "\nAssembly: {0}", Assembly);
if (!string.IsNullOrEmpty(DependencyPaths))
- LogLine(1, "DependencyPaths: \t{0}", DependencyPaths);
+ Logger.LogLine(1, "DependencyPaths: \t{0}", DependencyPaths);
if (!string.IsNullOrEmpty(ReferencePath))
- LogLine(1, "ReferencePath: \t{0}", ReferencePath.Replace("//", "/"));
- LogLine(3, "DebugSymbols:\"{0}\"", DebugSymbols);
+ Logger.LogLine(1, "ReferencePath: \t{0}", ReferencePath.Replace("//", "/"));
+ Logger.LogLine(3, "DebugSymbols:\"{0}\"", DebugSymbols);
var skipassembly = true; //change this to false to enable XamlC by default
bool success = true;
if (!File.Exists(Assembly))
{
- LogLine(1, "Assembly file not found. Skipping XamlC.");
+ Logger.LogLine(1, "Assembly file not found. Skipping XamlC.");
return true;
}
@@ -151,7 +44,7 @@ namespace Xamarin.Forms.Build.Tasks
{
foreach (var dep in DependencyPaths.Split(';'))
{
- LogLine(3, "Adding searchpath {0}", dep);
+ Logger.LogLine(3, "Adding searchpath {0}", dep);
resolver.AddSearchDirectory(dep);
}
}
@@ -162,7 +55,7 @@ namespace Xamarin.Forms.Build.Tasks
foreach (var p in paths)
{
var searchpath = Path.GetDirectoryName(p);
- LogLine(3, "Adding searchpath {0}", searchpath);
+ Logger.LogLine(3, "Adding searchpath {0}", searchpath);
resolver.AddSearchDirectory(searchpath);
}
}
@@ -201,21 +94,21 @@ namespace Xamarin.Forms.Build.Tasks
skipmodule = false;
}
- LogLine(2, " Module: {0}", module.Name);
+ Logger.LogLine(2, " Module: {0}", module.Name);
var resourcesToPrune = new List<EmbeddedResource>();
foreach (var resource in module.Resources.OfType<EmbeddedResource>())
{
- LogString(2, " Resource: {0}... ", resource.Name);
+ Logger.LogString(2, " Resource: {0}... ", resource.Name);
string classname;
if (!resource.IsXaml(out classname))
{
- LogLine(2, "skipped.");
+ Logger.LogLine(2, "skipped.");
continue;
}
TypeDefinition typeDef = module.GetType(classname);
if (typeDef == null)
{
- LogLine(2, "no type found... skipped.");
+ Logger.LogLine(2, "no type found... skipped.");
continue;
}
var skiptype = skipmodule;
@@ -236,61 +129,61 @@ namespace Xamarin.Forms.Build.Tasks
if (skiptype)
{
- LogLine(2, "Has XamlCompilationAttribute set to Skip and not Compile... skipped");
+ Logger.LogLine(2, "Has XamlCompilationAttribute set to Skip and not Compile... skipped");
continue;
}
var initComp = typeDef.Methods.FirstOrDefault(md => md.Name == "InitializeComponent");
if (initComp == null)
{
- LogLine(2, "no InitializeComponent found... skipped.");
+ Logger.LogLine(2, "no InitializeComponent found... skipped.");
continue;
}
- LogLine(2, "");
+ Logger.LogLine(2, "");
var initCompRuntime = typeDef.Methods.FirstOrDefault(md => md.Name == "__InitComponentRuntime");
if (initCompRuntime != null)
- LogLine(2, " __InitComponentRuntime already exists... not duplicating");
+ Logger.LogLine(2, " __InitComponentRuntime already exists... not duplicating");
else {
- LogString(2, " Duplicating {0}.InitializeComponent () into {0}.__InitComponentRuntime ... ", typeDef.Name);
+ Logger.LogString(2, " Duplicating {0}.InitializeComponent () into {0}.__InitComponentRuntime ... ", typeDef.Name);
initCompRuntime = DuplicateMethodDef(typeDef, initComp, "__InitComponentRuntime");
- LogLine(2, "done.");
+ Logger.LogLine(2, "done.");
}
- LogString(2, " Parsing Xaml... ");
+ Logger.LogString(2, " Parsing Xaml... ");
var rootnode = ParseXaml(resource.GetResourceStream(), typeDef);
if (rootnode == null)
{
- LogLine(2, "failed.");
+ Logger.LogLine(2, "failed.");
continue;
}
- LogLine(2, "done.");
+ Logger.LogLine(2, "done.");
hasCompiledXamlResources = true;
- LogString(2, " Replacing {0}.InitializeComponent ()... ", typeDef.Name);
+ Logger.LogString(2, " Replacing {0}.InitializeComponent ()... ", typeDef.Name);
Exception e;
if (!TryCoreCompile(initComp, initCompRuntime, rootnode, out e)) {
success = false;
- LogLine(2, "failed.");
+ Logger.LogLine(2, "failed.");
thrownExceptions?.Add(e);
- LogException(null, null, null, resource.Name, e);
- LogLine(4, e.StackTrace);
+ Logger.LogException(null, null, null, resource.Name, e);
+ Logger.LogLine(4, e.StackTrace);
continue;
}
- LogLine(2, "done.");
+ Logger.LogLine(2, "done.");
if (OptimizeIL)
{
- LogString(2, " Optimizing IL... ");
+ Logger.LogString(2, " Optimizing IL... ");
initComp.Body.OptimizeMacros();
- LogLine(2, "done");
+ Logger.LogLine(2, "done");
}
if (OutputGeneratedILAsCode)
{
var filepath = Path.Combine(Path.GetDirectoryName(Assembly), typeDef.FullName + ".decompiled.cs");
- LogString(2, " Decompiling {0} into {1}...", typeDef.FullName, filepath);
+ Logger.LogString(2, " Decompiling {0} into {1}...", typeDef.FullName, filepath);
var decompilerContext = new DecompilerContext(module);
using (var writer = new StreamWriter(filepath))
{
@@ -301,46 +194,46 @@ namespace Xamarin.Forms.Build.Tasks
codeDomBuilder.GenerateCode(output);
}
- LogLine(2, "done");
+ Logger.LogLine(2, "done");
}
resourcesToPrune.Add(resource);
}
if (!KeepXamlResources)
{
if (resourcesToPrune.Any())
- LogLine(2, " Removing compiled xaml resources");
+ Logger.LogLine(2, " Removing compiled xaml resources");
foreach (var resource in resourcesToPrune)
{
- LogString(2, " Removing {0}... ", resource.Name);
+ Logger.LogString(2, " Removing {0}... ", resource.Name);
module.Resources.Remove(resource);
- LogLine(2, "done");
+ Logger.LogLine(2, "done");
}
}
- LogLine(2, "");
+ Logger.LogLine(2, "");
}
if (!hasCompiledXamlResources)
{
- LogLine(1, "No compiled resources. Skipping writing assembly.");
+ Logger.LogLine(1, "No compiled resources. Skipping writing assembly.");
return success;
}
- LogString(1, "Writing the assembly... ");
+ Logger.LogString(1, "Writing the assembly... ");
try
{
assemblyDefinition.Write(Assembly, new WriterParameters
{
WriteSymbols = DebugSymbols
});
- LogLine(1, "done.");
+ Logger.LogLine(1, "done.");
}
catch (Exception e)
{
- LogLine(1, "failed.");
- LogException(null, null, null, null, e);
+ Logger.LogLine(1, "failed.");
+ Logger.LogException(null, null, null, null, e);
thrownExceptions?.Add(e);
- LogLine(4, e.StackTrace);
+ Logger.LogLine(4, e.StackTrace);
success = false;
}
@@ -393,7 +286,7 @@ namespace Xamarin.Forms.Build.Tasks
il.Append(nop);
}
- var visitorContext = new ILContext(il, body);
+ var visitorContext = new ILContext(il, body, body.Method.Module);
rootnode.Accept(new XamlNodeVisitor((node, parent) => node.Parent = parent), null);
rootnode.Accept(new ExpandMarkupsVisitor(visitorContext), null);
@@ -413,72 +306,5 @@ namespace Xamarin.Forms.Build.Tasks
return false;
}
}
-
- protected static MethodDefinition DuplicateMethodDef(TypeDefinition typeDef, MethodDefinition methodDef, string newName)
- {
- var dup = new MethodDefinition(newName, methodDef.Attributes, methodDef.ReturnType);
- dup.Body = methodDef.Body;
- typeDef.Methods.Add(dup);
- return dup;
- }
-
- static ILRootNode ParseXaml(Stream stream, TypeReference typeReference)
- {
- ILRootNode rootnode = null;
- using (var reader = XmlReader.Create(stream))
- {
- while (reader.Read())
- {
- //Skip until element
- if (reader.NodeType == XmlNodeType.Whitespace)
- continue;
- if (reader.NodeType != XmlNodeType.Element)
- {
- Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value);
- continue;
- }
-
- XamlParser.ParseXaml(
- rootnode = new ILRootNode(new XmlType(reader.NamespaceURI, reader.Name, null), typeReference, reader as IXmlNamespaceResolver), reader);
- break;
- }
- }
- return rootnode;
- }
- }
-
- static class CecilExtensions
- {
- public static bool IsXaml(this EmbeddedResource resource, out string classname)
- {
- classname = null;
- if (!resource.Name.EndsWith(".xaml", StringComparison.InvariantCulture))
- return false;
-
- using (var resourceStream = resource.GetResourceStream())
- {
- var xmlDoc = new XmlDocument();
- xmlDoc.Load(resourceStream);
-
- var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
-
- var root = xmlDoc.SelectSingleNode("/*", nsmgr);
- if (root == null)
- {
- // Log (2, "No root found... ");
- return false;
- }
-
- var rootClass = root.Attributes["Class", "http://schemas.microsoft.com/winfx/2006/xaml"] ??
- root.Attributes["Class", "http://schemas.microsoft.com/winfx/2009/xaml"];
- if (rootClass == null)
- {
- // Log (2, "no x:Class found... ");
- return false;
- }
- classname = rootClass.Value;
- return true;
- }
- }
}
} \ No newline at end of file
diff --git a/Xamarin.Forms.Build.Tasks/XamlTask.cs b/Xamarin.Forms.Build.Tasks/XamlTask.cs
new file mode 100644
index 00000000..d4864ac3
--- /dev/null
+++ b/Xamarin.Forms.Build.Tasks/XamlTask.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Xml;
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+using Mono.Cecil;
+
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.Build.Tasks
+{
+ public abstract class XamlTask : AppDomainIsolatedTask
+ {
+ [Required]
+ public string Assembly { get; set; }
+ public string DependencyPaths { get; set; }
+ public string ReferencePath { get; set; }
+ public int Verbosity { get; set; }
+ public bool DebugSymbols { get; set; }
+
+ internal XamlTask()
+ {
+ }
+
+ protected Logger Logger { get; set; }
+
+ public override bool Execute()
+ {
+ Logger = new Logger(Log, Verbosity);
+ return Execute(null);
+ }
+
+ public abstract bool Execute(IList<Exception> thrownExceptions);
+
+ protected static MethodDefinition DuplicateMethodDef(TypeDefinition typeDef, MethodDefinition methodDef, string newName)
+ {
+ var dup = new MethodDefinition(newName, methodDef.Attributes, methodDef.ReturnType);
+ dup.Body = methodDef.Body;
+ typeDef.Methods.Add(dup);
+ return dup;
+ }
+
+ internal static ILRootNode ParseXaml(Stream stream, TypeReference typeReference)
+ {
+ ILRootNode rootnode = null;
+ using (var reader = XmlReader.Create(stream)) {
+ while (reader.Read()) {
+ //Skip until element
+ if (reader.NodeType == XmlNodeType.Whitespace)
+ continue;
+ if (reader.NodeType != XmlNodeType.Element) {
+ Debug.WriteLine("Unhandled node {0} {1} {2}", reader.NodeType, reader.Name, reader.Value);
+ continue;
+ }
+
+ XamlParser.ParseXaml(
+ rootnode = new ILRootNode(new XmlType(reader.NamespaceURI, reader.Name, null), typeReference, reader as IXmlNamespaceResolver), reader);
+ break;
+ }
+ }
+ return rootnode;
+ }
+ }
+
+ static class CecilExtensions
+ {
+ public static bool IsXaml(this EmbeddedResource resource, out string classname)
+ {
+ classname = null;
+ if (!resource.Name.EndsWith(".xaml", StringComparison.InvariantCulture))
+ return false;
+
+ using (var resourceStream = resource.GetResourceStream()) {
+ var xmlDoc = new XmlDocument();
+ xmlDoc.Load(resourceStream);
+
+ var nsmgr = new XmlNamespaceManager(xmlDoc.NameTable);
+
+ var root = xmlDoc.SelectSingleNode("/*", nsmgr);
+ if (root == null)
+ return false;
+
+ var rootClass = root.Attributes ["Class", "http://schemas.microsoft.com/winfx/2006/xaml"] ??
+ root.Attributes ["Class", "http://schemas.microsoft.com/winfx/2009/xaml"];
+ if (rootClass == null)
+ return false;
+ classname = rootClass.Value;
+ return true;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla33870.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla33870.cs
index 888ddefe..6d382666 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla33870.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla33870.cs
@@ -44,7 +44,7 @@ namespace Xamarin.Forms.Controls
var listview = new ListView {
ItemsSource = source,
IsGroupingEnabled = true,
- GroupDisplayBinding = Binding.Create<Section> (x => x.Title),
+ GroupDisplayBinding = new Binding ("Title"),
};
var label = new Label { Text = "Tap CLEAR SELECTION. If the app does not crash and no item is selected, the test has passed." };
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla34720.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla34720.cs
index 15f601ae..a30083ea 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla34720.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla34720.cs
@@ -25,9 +25,9 @@ namespace Xamarin.Forms.Controls
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
};
- _list.SetBinding (ListView.ItemsSourceProperty, Binding.Create<TestListViewModel> (r => r.Items));
- _list.SetBinding (ListView.RefreshCommandProperty, Binding.Create<TestListViewModel> (r => r.RefreshCommand));
- _list.SetBinding (ListView.IsRefreshingProperty, Binding.Create<TestListViewModel> (r => r.IsRefreshing));
+ _list.SetBinding (ListView.ItemsSourceProperty, "Items");
+ _list.SetBinding (ListView.RefreshCommandProperty, "RefreshCommand");
+ _list.SetBinding (ListView.IsRefreshingProperty, "IsRefreshing");
var listViewModel = new TestListViewModel ();
listViewModel.AddTestData ();
@@ -150,7 +150,7 @@ namespace Xamarin.Forms.Controls
HorizontalOptions = LayoutOptions.StartAndExpand
};
Grid.SetColumnSpan (materialNumber, 2);
- materialNumber.SetBinding (Label.TextProperty, Binding.Create<TestViewModel> (vm => vm.Number));
+ materialNumber.SetBinding (Label.TextProperty, "Number");
grid.Children.Add (materialNumber);
//2 Description
@@ -160,7 +160,7 @@ namespace Xamarin.Forms.Controls
};
Grid.SetColumnSpan (materialDescription, 2);
Grid.SetRow (materialDescription, 1);
- materialDescription.SetBinding (Label.TextProperty, Binding.Create<TestViewModel> (vm => vm.Description));
+ materialDescription.SetBinding (Label.TextProperty, "Description");
//grid.Children.Add (materialDescription);
//3 Approve Label
@@ -170,7 +170,7 @@ namespace Xamarin.Forms.Controls
};
Grid.SetColumn (canApprove, 1);
Grid.SetRow (canApprove, 1);
- canApprove.SetBinding (Label.TextProperty, Binding.Create<TestViewModel> (vm => vm.CanApprove, stringFormat: "Can Approve: {0}"));
+ canApprove.SetBinding (Label.TextProperty, new Binding ("CanApprove", stringFormat: "Can Approve: {0}"));
grid.Children.Add (canApprove);
//3 Approve Label
@@ -180,7 +180,7 @@ namespace Xamarin.Forms.Controls
};
Grid.SetColumn (canDeny, 0);
Grid.SetRow (canDeny, 1);
- canDeny.SetBinding (Label.TextProperty, Binding.Create<TestViewModel> (vm => vm.CanDeny, stringFormat: "Can Deny: {0}"));
+ canDeny.SetBinding (Label.TextProperty, new Binding ("CanDeny", stringFormat: "Can Deny: {0}"));
grid.Children.Add (canDeny);
Content = grid;
@@ -218,7 +218,7 @@ namespace Xamarin.Forms.Controls
VerticalOptions = LayoutOptions.FillAndExpand
};
- denyBtn.SetBinding (Button.CommandProperty, Binding.Create<TestViewModel> (r => r.DenyCommand));
+ denyBtn.SetBinding(Button.CommandProperty, "DenyCommand");
grid.Children.Add (denyBtn);
@@ -231,7 +231,7 @@ namespace Xamarin.Forms.Controls
};
Grid.SetColumn (approveBtn, 1);
- approveBtn.SetBinding (Button.CommandProperty, Binding.Create<TestViewModel> (r => r.ApproveCommand));
+ approveBtn.SetBinding (Button.CommandProperty, "ApproveCommand");
grid.Children.Add (approveBtn);
@@ -239,10 +239,7 @@ namespace Xamarin.Forms.Controls
overallGrid.Children.Add (grid);
Content = overallGrid;
}
-
}
-
-
}
[Preserve (AllMembers = true)]
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla36009.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla36009.cs
index f5255279..2ef5afac 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla36009.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla36009.cs
@@ -31,7 +31,7 @@ namespace Xamarin.Forms.Controls
Content = boxview
};
- contentView.SetBinding (IsVisibleProperty, Binding.Create<SampleViewModel> (t => t.IsContentVisible));
+ contentView.SetBinding (IsVisibleProperty, "IsContentVisible");
var layout = new AbsoluteLayout {
Children = { contentView }
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1400.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1400.cs
index 287ebe08..e120e768 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1400.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1400.cs
@@ -30,7 +30,7 @@ namespace Xamarin.Forms.Controls
Editfield = new Entry();
Editfield.HorizontalOptions = LayoutOptions.FillAndExpand;
Editfield.BindingContext = Data.First().First();
- Editfield.SetBinding<MyData>(Entry.TextProperty, f => f.Title);
+ Editfield.SetBinding(Entry.TextProperty, "Title");
Editfield.TextChanged += (sender, args) =>
{
@@ -137,7 +137,7 @@ namespace Xamarin.Forms.Controls
public VCTest()
{
var label = new Label();
- label.SetBinding<MyData>(Label.TextProperty, f => f.Title);
+ label.SetBinding(Label.TextProperty, "Title");
View = label;
}
}
@@ -147,7 +147,7 @@ namespace Xamarin.Forms.Controls
public VCHeader()
{
var label = new Label();
- label.SetBinding<MyGroup>(Label.TextProperty, f => f.Headertitle);
+ label.SetBinding(Label.TextProperty, "Headertitle");
View = label;
}
}
diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1762.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1762.cs
index b945c9cd..84e2a3b2 100644
--- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1762.cs
+++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue1762.cs
@@ -28,8 +28,8 @@ namespace Xamarin.Forms.Controls
ItemsSource = Objs,
ItemTemplate = new DataTemplate (() => {
SwitchCell cell = new SwitchCell ();
- cell.SetBinding<MyObj> (SwitchCell.TextProperty, m => m.DisplayText);
- cell.SetBinding<MyObj> (SwitchCell.OnProperty, m => m.IsSelected);
+ cell.SetBinding (SwitchCell.TextProperty, "DisplayText");
+ cell.SetBinding (SwitchCell.OnProperty, "IsSelected");
return cell;
}),
IsGroupingEnabled = true,
diff --git a/Xamarin.Forms.Controls/ControlGalleryPages/ListRefresh.cs b/Xamarin.Forms.Controls/ControlGalleryPages/ListRefresh.cs
index 28afd7be..cad0638f 100644
--- a/Xamarin.Forms.Controls/ControlGalleryPages/ListRefresh.cs
+++ b/Xamarin.Forms.Controls/ControlGalleryPages/ListRefresh.cs
@@ -57,8 +57,8 @@ namespace Xamarin.Forms.Controls
stack.Children.Add (lbl);
lv.Header = new ContentView { HeightRequest = 300, HorizontalOptions = LayoutOptions.FillAndExpand, Content = stack };
- lv.SetBinding<FooViewModel> (ListView.ItemsSourceProperty, m => m.Things);
- lv.SetBinding<FooViewModel> (ListView.RefreshCommandProperty, m => m.RefreshThingsCommand);
+ lv.SetBinding (ListView.ItemsSourceProperty, "Things");
+ lv.SetBinding (ListView.RefreshCommandProperty, "RefreshThingsCommand");
grid.Children.Add (lv, 0, 0);
Content = grid;
diff --git a/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs b/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs
index dd0d2244..84addb7a 100644
--- a/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/BindingBaseUnitTests.cs
@@ -1,12 +1,80 @@
using System;
+using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
+using System.Runtime.CompilerServices;
using NUnit.Framework;
namespace Xamarin.Forms.Core.UnitTests
{
public abstract class BindingBaseUnitTests : BaseTestFixture
{
- protected abstract BindingBase CreateBinding (BindingMode mode, string stringFormat = null);
+ internal class Logger : LogListener
+ {
+ public IReadOnlyList<string> Messages {
+ get { return messages; }
+ }
+
+ public override void Warning(string category, string message)
+ {
+ messages.Add("[" + category + "] " + message);
+ }
+
+ readonly List<string> messages = new List<string>();
+ }
+
+ internal Logger log;
+
+ protected abstract BindingBase CreateBinding (BindingMode mode = BindingMode.Default, string stringFormat = null);
+
+ internal class ComplexMockViewModel : MockViewModel
+ {
+ public ComplexMockViewModel Model {
+ get { return model; }
+ set {
+ if (model == value)
+ return;
+
+ model = value;
+ OnPropertyChanged("Model");
+ }
+ }
+
+ internal int count;
+ public int QueryCount {
+ get { return count++; }
+ }
+
+ [IndexerName("Indexer")]
+ public string this [int v] {
+ get { return values [v]; }
+ set {
+ if (values [v] == value)
+ return;
+
+ values [v] = value;
+ OnPropertyChanged("Indexer[" + v + "]");
+ }
+ }
+
+ public string [] Array {
+ get;
+ set;
+ }
+
+ public object DoStuff()
+ {
+ return null;
+ }
+
+ public object DoStuff(object argument)
+ {
+ return null;
+ }
+
+ string [] values = new string [5];
+ ComplexMockViewModel model;
+ }
[Test]
public void CloneMode()
@@ -20,8 +88,7 @@ namespace Xamarin.Forms.Core.UnitTests
[Test]
public void StringFormat()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = CreateBinding (BindingMode.Default, "Foo {0}");
var vm = new MockViewModel { Text = "Bar" };
@@ -34,8 +101,7 @@ namespace Xamarin.Forms.Core.UnitTests
[Test]
public void StringFormatOnUpdate()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = CreateBinding (BindingMode.Default, "Foo {0}");
var vm = new MockViewModel { Text = "Bar" };
@@ -51,8 +117,7 @@ namespace Xamarin.Forms.Core.UnitTests
[Description ("StringFormat should not be applied to OneWayToSource bindings")]
public void StringFormatOneWayToSource()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = CreateBinding (BindingMode.OneWayToSource, "Foo {0}");
var vm = new MockViewModel { Text = "Bar" };
@@ -68,8 +133,7 @@ namespace Xamarin.Forms.Core.UnitTests
[Description ("StringFormat should only be applied from from source in TwoWay bindings")]
public void StringFormatTwoWay()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = CreateBinding (BindingMode.TwoWay, "Foo {0}");
var vm = new MockViewModel { Text = "Bar" };
@@ -86,8 +150,7 @@ namespace Xamarin.Forms.Core.UnitTests
[Description ("You should get an exception when trying to change a binding after it's been applied")]
public void ChangeAfterApply()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = CreateBinding (BindingMode.OneWay);
var vm = new MockViewModel { Text = "Bar" };
@@ -97,6 +160,466 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.That (() => binding.Mode = BindingMode.OneWayToSource, Throws.InvalidOperationException);
Assert.That (() => binding.StringFormat = "{0}", Throws.InvalidOperationException);
}
+
+ [Test]
+ public void StringFormatNonStringType()
+ {
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var binding = new Binding("Value", stringFormat: "{0:P2}");
+
+ var vm = new { Value = 0.95d };
+ var bo = new MockBindable { BindingContext = vm };
+ bo.SetBinding(property, binding);
+
+ if (System.Threading.Thread.CurrentThread.CurrentCulture.Name == "tr-TR")
+ Assert.That(bo.GetValue(property), Is.EqualTo("%95,00"));
+ else
+ Assert.That(bo.GetValue(property), Is.EqualTo("95.00 %"));
+ }
+
+ [Test]
+ public void ReuseBindingInstance()
+ {
+ var vm = new MockViewModel();
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = vm;
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var binding = new Binding("Text");
+ bindable.SetBinding(property, binding);
+
+ var bindable2 = new MockBindable();
+ bindable2.BindingContext = new MockViewModel();
+ Assert.Throws<InvalidOperationException>(() => bindable2.SetBinding(property, binding),
+ "Binding allowed reapplication with a different context");
+ }
+
+ [Test, Category("[Binding] Set Value")]
+ public void ValueSetOnOneWay(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new MockViewModel {
+ Text = value
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), null, propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Set Value")]
+ public void ValueSetOnOneWayToSource(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new MockViewModel();
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), defaultValue: value, defaultBindingMode: propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property changed");
+ Assert.AreEqual(value, viewmodel.Text,
+ "BindingContext property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Set Value")]
+ public void ValueSetOnTwoWay(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new MockViewModel {
+ Text = value
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), defaultValue: "default value", defaultBindingMode: propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Update Value")]
+ public void ValueUpdatedWithSimplePathOnOneWayBinding(
+ [Values(true, false)]bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Update Value")]
+ public void ValueUpdatedWithSimplePathOnOneWayToSourceBinding(
+ [Values(true, false)]bool isDefault)
+
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ string original = (string)bindable.GetValue(property);
+ const string value = "value";
+ viewmodel.Text = value;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource");
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Update Value")]
+ public void ValueUpdatedWithSimplePathOnTwoWayBinding(
+ [Values(true, false)]bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, viewmodel.Text,
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Text,
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ bindable.BindingContext = new MockViewModel();
+ Assert.AreEqual(null, bindable.GetValue(property));
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(null, bindable.GetValue(property),
+ "Target updated from old Source property change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithOldContextDoesNotUpdateWithTwoWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ bindable.BindingContext = new MockViewModel();
+ Assert.AreEqual(null, bindable.GetValue(property));
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(null, bindable.GetValue(property),
+ "Target updated from old Source property change");
+
+ string original = viewmodel.Text;
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(original, viewmodel.Text,
+ "Source updated from old Target property change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayToSourceBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = CreateBinding(bindingMode);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ bindable.BindingContext = new MockViewModel();
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property));
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property),
+ "Target updated from old Source property change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ public void BindingStaysOnUpdateValueFromBinding()
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new MockViewModel {
+ Text = "Foo"
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), null);
+ var binding = CreateBinding(BindingMode.Default);
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property));
+
+ const string newValue2 = "new value 2";
+ viewmodel.Text = newValue2;
+ Assert.AreEqual(newValue2, bindable.GetValue(property));
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ public void OneWayToSourceContextSetToNull()
+ {
+ var binding = new Binding("Text", BindingMode.OneWayToSource);
+
+ MockBindable bindable = new MockBindable {
+ BindingContext = new MockViewModel()
+ };
+ bindable.SetBinding(MockBindable.TextProperty, binding);
+
+ Assert.That(() => bindable.BindingContext = null, Throws.Nothing);
+ }
+
+ [Category("[Binding] Simple paths")]
+ [TestCase(BindingMode.OneWay)]
+ [TestCase(BindingMode.OneWayToSource)]
+ [TestCase(BindingMode.TwoWay)]
+ public void SourceAndTargetAreWeakWeakSimplePath(BindingMode mode)
+ {
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", BindingMode.OneWay);
+ var binding = CreateBinding(mode);
+
+ WeakReference weakViewModel = null, weakBindable = null;
+
+ int i = 0;
+ Action create = null;
+ create = () => {
+ if (i++ < 1024) {
+ create();
+ return;
+ }
+
+ MockBindable bindable = new MockBindable();
+ weakBindable = new WeakReference(bindable);
+
+ MockViewModel viewmodel = new MockViewModel();
+ weakViewModel = new WeakReference(viewmodel);
+
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ Assume.That(() => bindable.BindingContext = null, Throws.Nothing);
+ };
+
+ create();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWay)
+ Assert.IsFalse(weakViewModel.IsAlive, "ViewModel wasn't collected");
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource)
+ Assert.IsFalse(weakBindable.IsAlive, "Bindable wasn't collected");
+ }
+
+ [Test]
+ public void PropertyChangeBindingsOccurThroughMainThread()
+ {
+ var vm = new MockViewModel { Text = "text" };
+
+ var bindable = new MockBindable();
+ var binding = CreateBinding();
+ bindable.BindingContext = vm;
+ bindable.SetBinding(MockBindable.TextProperty, binding);
+
+ bool mainThread = false;
+ Device.PlatformServices = new MockPlatformServices(invokeOnMainThread: a => mainThread = true);
+
+ vm.Text = "updated";
+
+ Assert.IsTrue(mainThread, "Binding did not occur on main thread");
+ Assert.AreNotEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty), "Binding was applied anyway through other means");
+ }
}
[TestFixture]
@@ -223,5 +746,6 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.That (() => BindingBase.TryGetSynchronizedCollection (null, out context),
Throws.InstanceOf<ArgumentNullException>());
}
+
}
}
diff --git a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs
index c28adda9..548e4ad5 100644
--- a/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/BindingUnitTests.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Globalization;
using System.ComponentModel;
using System.Linq;
@@ -16,24 +15,6 @@ namespace Xamarin.Forms.Core.UnitTests
public class BindingUnitTests
: BindingBaseUnitTests
{
- class Logger
- : LogListener
- {
- public IReadOnlyList<string> Messages
- {
- get { return messages; }
- }
-
- public override void Warning (string category, string message)
- {
- messages.Add ("[" + category + "] " + message);
- }
-
- readonly List<string> messages = new List<string>();
- }
-
- Logger log;
-
[SetUp]
public override void Setup()
{
@@ -52,7 +33,7 @@ namespace Xamarin.Forms.Core.UnitTests
Log.Listeners.Remove (log);
}
- protected override BindingBase CreateBinding (BindingMode mode, string stringFormat = null)
+ protected override BindingBase CreateBinding(BindingMode mode = BindingMode.Default, string stringFormat = null)
{
return new Binding ("Text", mode, stringFormat: stringFormat);
}
@@ -87,9 +68,8 @@ namespace Xamarin.Forms.Core.UnitTests
[Description ("You should get an exception when trying to change a binding after it's been applied")]
public void ChangeBindingAfterApply()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
- var binding = new Binding { Path = "Text" };
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var binding = (Binding)CreateBinding(BindingMode.Default, "Foo {0}");
var vm = new MockViewModel { Text = "Bar" };
var bo = new MockBindable { BindingContext = vm };
@@ -103,59 +83,13 @@ namespace Xamarin.Forms.Core.UnitTests
[Test]
public void NullPathIsSelf()
{
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
var binding = new Binding();
var bo = new MockBindable { BindingContext = "Foo" };
- bo.SetBinding (property, binding);
-
- Assert.That (bo.GetValue (property), Is.EqualTo ("Foo"));
- }
+ bo.SetBinding(property, binding);
- class DoubleViewModel
- : MockViewModel
- {
- public double Value
- {
- get;
- set;
- }
- }
-
- [Test]
- public void StringFormatNonStringType()
- {
- var property = BindableProperty.Create<MockBindable, string> (w => w.Foo, null);
-
- var binding = new Binding ("Value", stringFormat: "{0:P2}");
-
- var vm = new DoubleViewModel { Value = 0.95 };
- var bo = new MockBindable { BindingContext = vm };
- bo.SetBinding (property, binding);
-
- if (System.Threading.Thread.CurrentThread.CurrentCulture.Name == "tr-TR")
- Assert.That (bo.GetValue (property), Is.EqualTo ("%95,00"));
- else
- Assert.That (bo.GetValue (property), Is.EqualTo ("95.00 %"));
- }
-
- [Test]
- public void ReuseBindingInstance()
- {
- var vm = new MockViewModel();
-
- var bindable = new MockBindable();
- bindable.BindingContext = vm;
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null);
- var binding = new Binding ("Text");
- bindable.SetBinding (property, binding);
-
- var bindable2 = new MockBindable();
- bindable2.BindingContext = new MockViewModel();
- Assert.Throws<InvalidOperationException> (() => bindable2.SetBinding (property, binding),
- "Binding allowed reapplication with a different context");
+ Assert.That(bo.GetValue(property), Is.EqualTo("Foo"));
}
class ComplexPropertyNamesViewModel
@@ -204,118 +138,6 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.That (bindable.Text, Is.EqualTo ("Value"));
}
- [Test, Category ("[Binding] Simple paths")]
- public void ValueSetOnOneWayWithSimplePathBinding (
- [Values (true, false)] bool setContextFirst,
- [Values (true, false)] bool isDefault)
- {
- const string value = "Foo";
- var viewmodel = new MockViewModel {
- Text = value
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWay;
- if (isDefault) {
- propertyDefault = BindingMode.OneWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null, propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- if (setContextFirst) {
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
- } else {
- bindable.SetBinding (property, binding);
- bindable.BindingContext = viewmodel;
- }
-
- Assert.AreEqual (value, viewmodel.Text,
- "BindingContext property changed");
- Assert.AreEqual (value, bindable.GetValue (property),
- "Target property did not change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Test, Category ("[Binding] Simple paths")]
- public void ValueSetOnOneWayToSourceWithSimplePathBinding (
- [Values (true, false)] bool setContextFirst,
- [Values (true, false)] bool isDefault)
- {
- const string value = "Foo";
- var viewmodel = new MockViewModel();
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWayToSource;
- if (isDefault) {
- propertyDefault = BindingMode.OneWayToSource;
- bindingMode = BindingMode.Default;
- }
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text,
- defaultValue: value, defaultBindingMode: propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- if (setContextFirst) {
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
- } else {
- bindable.SetBinding (property, binding);
- bindable.BindingContext = viewmodel;
- }
-
- Assert.AreEqual (value, bindable.GetValue (property),
- "Target property changed");
- Assert.AreEqual (value, viewmodel.Text,
- "BindingContext property did not change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Test, Category ("[Binding] Simple paths")]
- public void ValueSetOnTwoWayWithSimplePathBinding (
- [Values (true, false)] bool setContextFirst,
- [Values (true, false)] bool isDefault)
- {
- const string value = "Foo";
- var viewmodel = new MockViewModel {
- Text = value
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.TwoWay;
- if (isDefault) {
- propertyDefault = BindingMode.TwoWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- if (setContextFirst) {
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
- } else {
- bindable.SetBinding (property, binding);
- bindable.BindingContext = viewmodel;
- }
-
- Assert.AreEqual (value, viewmodel.Text,
- "BindingContext property changed");
- Assert.AreEqual (value, bindable.GetValue (property),
- "Target property did not change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
[Test]
[Category ("[Binding] Complex paths")]
public void ValueSetOnOneWayWithComplexPathBinding (
@@ -338,9 +160,8 @@ namespace Xamarin.Forms.Core.UnitTests
bindingMode = BindingMode.Default;
}
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null, propertyDefault);
-
- var binding = new Binding ("Model.Model.Text", bindingMode);
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), null, propertyDefault);
+ var binding = new Binding("Model.Model.Text", bindingMode);
var bindable = new MockBindable();
if (setContextFirst) {
@@ -379,10 +200,8 @@ namespace Xamarin.Forms.Core.UnitTests
bindingMode = BindingMode.Default;
}
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text,
- defaultValue: value, defaultBindingMode: propertyDefault);
-
- var binding = new Binding ("Model.Model.Text", bindingMode);
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), value, propertyDefault);
+ var binding = new Binding("Model.Model.Text", bindingMode);
var bindable = new MockBindable();
if (setContextFirst) {
@@ -422,9 +241,8 @@ namespace Xamarin.Forms.Core.UnitTests
bindingMode = BindingMode.Default;
}
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var binding = new Binding ("Model.Model.Text", bindingMode);
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new Binding("Model.Model.Text", bindingMode);
var bindable = new MockBindable();
if (setContextFirst) {
@@ -470,10 +288,10 @@ namespace Xamarin.Forms.Core.UnitTests
[Values (true, false)] bool usePrivateSetter)
{
var value = "FooBar";
- var property = BindableProperty.Create<MockBindable, string> (w => w.Text, "default value", BindingMode.Default);
- var binding = new Binding (usePrivateSetter? "PropertyWithPrivateSetter.GetSetProperty": "PropertyWithPublicSetter.GetSetProperty", bindingmode);
- var viewmodel = new Outer (new Inner (value));
- var bindable = new MockBindable ();
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", BindingMode.Default);
+ var binding = new Binding(usePrivateSetter ? "PropertyWithPrivateSetter.GetSetProperty" : "PropertyWithPublicSetter.GetSetProperty", bindingmode);
+ var viewmodel = new Outer(new Inner(value));
+ var bindable = new MockBindable();
if (setContextFirst) {
bindable.BindingContext = viewmodel;
@@ -884,142 +702,32 @@ namespace Xamarin.Forms.Core.UnitTests
bindingMode = BindingMode.Default;
}
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
- var binding = new Binding (".", bindingMode);
+ var binding = new Binding(".", bindingMode);
const string value = "Foo";
var bindable = new MockBindable();
if (setContextFirst) {
bindable.BindingContext = value;
- bindable.SetBinding (property, binding);
+ bindable.SetBinding(property, binding);
} else {
- bindable.SetBinding (property, binding);
+ bindable.SetBinding(property, binding);
bindable.BindingContext = value;
}
- Assert.AreEqual (value, bindable.BindingContext,
+ Assert.AreEqual(value, bindable.BindingContext,
"BindingContext property changed");
- Assert.AreEqual (value, bindable.GetValue (property),
+ Assert.AreEqual(value, bindable.GetValue(property),
"Target property did not change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
"An error was logged: " + log.Messages.FirstOrDefault());
}
- [Category ("[Binding] Simple paths")]
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithSimplePathOnOneWayBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWay;
- if (isDefault) {
- propertyDefault = BindingMode.OneWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, new Binding ("Text", bindingMode));
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (newvalue, bindable.GetValue (property),
- "Bindable did not update on binding context property change");
- Assert.AreEqual (newvalue, viewmodel.Text,
- "Source property changed when it shouldn't");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Category ("[Binding] Simple paths")]
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithSimplePathOnOneWayToSourceBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWayToSource;
- if (isDefault) {
- propertyDefault = BindingMode.OneWayToSource;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, new Binding ("Text", bindingMode));
-
- string original = (string)bindable.GetValue (property);
- const string value = "value";
- viewmodel.Text = value;
- Assert.AreEqual (original, bindable.GetValue (property),
- "Target updated from Source on OneWayToSource");
-
- bindable.SetValue (property, newvalue);
- Assert.AreEqual (newvalue, bindable.GetValue (property),
- "Bindable did not update on binding context property change");
- Assert.AreEqual (newvalue, viewmodel.Text,
- "Source property changed when it shouldn't");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Category ("[Binding] Simple paths")]
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithSimplePathOnTwoWayBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.TwoWay;
- if (isDefault) {
- propertyDefault = BindingMode.TwoWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, new Binding ("Text", bindingMode));
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (newvalue, bindable.GetValue (property),
- "Target property did not update change");
- Assert.AreEqual (newvalue, viewmodel.Text,
- "Source property changed from what it was set to");
-
- const string newvalue2 = "New Value in the other direction";
-
- bindable.SetValue (property, newvalue2);
- Assert.AreEqual (newvalue2, viewmodel.Text,
- "Source property did not update with Target's change");
- Assert.AreEqual (newvalue2, bindable.GetValue (property),
- "Target property changed from what it was set to");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Category ("[Binding] Complex paths")]
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithComplexPathOnOneWayBinding (bool isDefault)
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnOneWayBinding(bool isDefault)
{
const string newvalue = "New Value";
var viewmodel = new ComplexMockViewModel {
@@ -1373,316 +1081,68 @@ namespace Xamarin.Forms.Core.UnitTests
bindingMode = BindingMode.Default;
}
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
+ var property = BindableProperty.Create<MockBindable, string>(w => w.Text, "default value", propertyDefault);
- var binding = new Binding (".", bindingMode);
+ var binding = new Binding(".", bindingMode);
var bindable = new MockBindable();
bindable.BindingContext = "value";
- bindable.SetBinding (property, binding);
+ bindable.SetBinding(property, binding);
const string newvalue = "New Value";
bindable.BindingContext = newvalue;
- Assert.AreEqual (newvalue, bindable.GetValue (property),
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
"Target property did not update change");
- Assert.AreEqual (newvalue, bindable.BindingContext,
+ Assert.AreEqual(newvalue, bindable.BindingContext,
"Source property changed from what it was set to");
const string newvalue2 = "New Value in the other direction";
- bindable.SetValue (property, newvalue2);
- Assert.AreEqual (newvalue, bindable.BindingContext,
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue, bindable.BindingContext,
"Self-path Source changed with Target's change");
- Assert.AreEqual (newvalue2, bindable.GetValue (property),
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
"Target property changed from what it was set to");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWay;
- if (isDefault) {
- propertyDefault = BindingMode.OneWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
-
- bindable.BindingContext = new MockViewModel();
- Assert.AreEqual (null, bindable.GetValue (property));
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (null, bindable.GetValue (property),
- "Target updated from old Source property change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithOldContextDoesNotUpdateWithTwoWayBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.TwoWay;
- if (isDefault) {
- propertyDefault = BindingMode.TwoWay;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
-
- bindable.BindingContext = new MockViewModel();
- Assert.AreEqual (null, bindable.GetValue (property));
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (null, bindable.GetValue (property),
- "Target updated from old Source property change");
-
- string original = viewmodel.Text;
-
- bindable.SetValue (property, newvalue);
- Assert.AreEqual (original, viewmodel.Text,
- "Source updated from old Target property change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [TestCase (true)]
- [TestCase (false)]
- public void ValueUpdatedWithOldContextDoesNotUpdateWithOneWayToSourceBinding (bool isDefault)
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- BindingMode propertyDefault = BindingMode.OneWay;
- BindingMode bindingMode = BindingMode.OneWayToSource;
- if (isDefault) {
- propertyDefault = BindingMode.OneWayToSource;
- bindingMode = BindingMode.Default;
- }
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", propertyDefault);
-
- var binding = new Binding ("Text", bindingMode);
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
-
- bindable.BindingContext = new MockViewModel();
- Assert.AreEqual (property.DefaultValue, bindable.GetValue (property));
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (property.DefaultValue, bindable.GetValue (property),
- "Target updated from old Source property change");
- Assert.That (log.Messages.Count, Is.EqualTo (0),
- "An error was logged: " + log.Messages.FirstOrDefault());
- }
-
- [Test]
- public void BindingStaysOnUpdateValueFromBinding()
- {
- const string newvalue = "New Value";
- var viewmodel = new MockViewModel {
- Text = "Foo"
- };
-
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, null);
-
- var binding = new Binding ("Text");
-
- var bindable = new MockBindable();
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
-
- viewmodel.Text = newvalue;
- Assert.AreEqual (newvalue, bindable.GetValue (property));
-
- const string newValue2 = "new value 2";
- viewmodel.Text = newValue2;
- Assert.AreEqual (newValue2, bindable.GetValue (property));
-
- Assert.That (log.Messages.Count, Is.EqualTo (0),
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
"An error was logged: " + log.Messages.FirstOrDefault());
}
- [Test]
- public void OneWayToSourceContextSetToNull()
- {
- var binding = new Binding ("Text", BindingMode.OneWayToSource);
-
- MockBindable bindable = new MockBindable {
- BindingContext = new MockViewModel()
- };
- bindable.SetBinding (MockBindable.TextProperty, binding);
-
- Assert.That (() => bindable.BindingContext = null, Throws.Nothing);
- }
-
- [Category ("[Binding] Simple paths")]
- [TestCase (BindingMode.OneWay)]
- [TestCase (BindingMode.OneWayToSource)]
- [TestCase (BindingMode.TwoWay)]
- public void SourceAndTargetAreWeakWeakSimplePath (BindingMode mode)
+ [Category("[Binding] Complex paths")]
+ [TestCase(BindingMode.OneWay)]
+ [TestCase(BindingMode.OneWayToSource)]
+ [TestCase(BindingMode.TwoWay)]
+ public void SourceAndTargetAreWeakComplexPath(BindingMode mode)
{
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value", BindingMode.OneWay);
+ var property = BindableProperty.Create<MockBindable, string>(w => w.Text, "default value");
- var binding = new Binding ("Text", mode);
+ var binding = new Binding("Model.Model[1]");
WeakReference weakViewModel = null, weakBindable = null;
- int i = 0;
- Action create = null;
- create = () => {
- if (i++ < 1024) {
- create();
- return;
- }
-
- MockBindable bindable = new MockBindable();
- weakBindable = new WeakReference (bindable);
-
- MockViewModel viewmodel = new MockViewModel();
- weakViewModel = new WeakReference (viewmodel);
-
- bindable.BindingContext = viewmodel;
- bindable.SetBinding (property, binding);
-
- Assume.That (() => bindable.BindingContext = null, Throws.Nothing);
- };
-
- create();
+ HackAroundMonoSucking(0, property, binding, out weakViewModel, out weakBindable);
GC.Collect();
GC.WaitForPendingFinalizers();
if (mode == BindingMode.TwoWay || mode == BindingMode.OneWay)
- Assert.IsFalse (weakViewModel.IsAlive, "ViewModel wasn't collected");
-
- if (mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource)
- Assert.IsFalse (weakBindable.IsAlive, "Bindable wasn't collected");
- }
+ Assert.IsFalse(weakViewModel.IsAlive, "ViewModel wasn't collected");
- internal class ComplexMockViewModel
- : MockViewModel
- {
- public ComplexMockViewModel Model
- {
- get { return model; }
- set
- {
- if (model == value)
- return;
-
- model = value;
- OnPropertyChanged ("Model");
- }
- }
-
- internal int count;
- public int QueryCount
- {
- get { return count++; }
- }
-
- [IndexerName ("Indexer")]
- public string this [int v]
- {
- get { return values[v]; }
- set
- {
- if (values[v] == value)
- return;
-
- values[v] = value;
- OnPropertyChanged ("Indexer[" + v + "]");
- }
- }
-
- public string[] Array
- {
- get;
- set;
- }
-
- public object DoStuff()
- {
- return null;
- }
-
- public object DoStuff (object argument)
- {
- return null;
- }
-
- string[] values = new string[5];
- ComplexMockViewModel model;
- }
-
- [Category ("[Binding] Complex paths")]
- [TestCase (BindingMode.OneWay)]
- [TestCase (BindingMode.OneWayToSource)]
- [TestCase (BindingMode.TwoWay)]
- public void SourceAndTargetAreWeakComplexPath (BindingMode mode)
- {
- var property = BindableProperty.Create<MockBindable, string> (w=>w.Text, "default value");
-
- var binding = new Binding ("Model.Model[1]");
-
- WeakReference weakViewModel = null, weakBindable = null;
-
- HackAroundMonoSucking (0, property, binding, out weakViewModel, out weakBindable);
-
- GC.Collect();
- GC.WaitForPendingFinalizers();
-
- if (mode == BindingMode.TwoWay || mode == BindingMode.OneWay)
- Assert.IsFalse (weakViewModel.IsAlive, "ViewModel wasn't collected");
-
if (mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource)
- Assert.IsFalse (weakBindable.IsAlive, "Bindable wasn't collected");
+ Assert.IsFalse(weakBindable.IsAlive, "Bindable wasn't collected");
}
// Mono doesn't handle the GC properly until the stack frame where the object is created is popped.
// This means calling another method and not just using lambda as works in real .NET
- void HackAroundMonoSucking (int i, BindableProperty property, Binding binding, out WeakReference weakViewModel, out WeakReference weakBindable)
+ void HackAroundMonoSucking(int i, BindableProperty property, Binding binding, out WeakReference weakViewModel, out WeakReference weakBindable)
{
if (i++ < 1024) {
- HackAroundMonoSucking (i, property, binding, out weakViewModel, out weakBindable);
+ HackAroundMonoSucking(i, property, binding, out weakViewModel, out weakBindable);
return;
}
MockBindable bindable = new MockBindable();
- weakBindable = new WeakReference (bindable);
+ weakBindable = new WeakReference(bindable);
ComplexMockViewModel viewmodel = new ComplexMockViewModel {
Model = new ComplexMockViewModel {
@@ -1719,16 +1179,18 @@ namespace Xamarin.Forms.Core.UnitTests
{
var converter = new TestConverter<string, int>();
- var vm = new MockViewModel { Text = "1" };
- var property = BindableProperty.Create<MockBindable, int> (w=>w.TargetInt, 0);
+ var vm = new MockViewModel ("1");
+ var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 0);
+ var binding = CreateBinding();
+ ((Binding)binding).Converter = converter;
var bindable = new MockBindable();
- bindable.SetBinding (property, new Binding ("Text", converter: converter));
+ bindable.SetBinding(property, binding);
bindable.BindingContext = vm;
- Assert.AreEqual (1, bindable.GetValue (property));
+ Assert.AreEqual(1, bindable.GetValue(property));
- Assert.That (log.Messages.Count, Is.EqualTo (0),
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
"An error was logged: " + log.Messages.FirstOrDefault());
}
@@ -2035,71 +1497,50 @@ namespace Xamarin.Forms.Core.UnitTests
string text = "foo";
- public string Text
- {
+ public string Text {
get { return text; }
}
- public string Text2
- {
+ public string Text2 {
set { text = value; }
}
- public string PrivateSetter
- {
+ public string PrivateSetter {
get;
private set;
}
}
[Test]
- [Description ("Paths should not distinguish types, a context change to a completely different type should work.")]
+ [Description("Paths should not distinguish types, a context change to a completely different type should work.")]
public void DifferentContextTypeAccessedCorrectlyWithSamePath()
{
var vm = new MockViewModel { Text = "text" };
var bindable = new MockBindable();
bindable.BindingContext = vm;
- bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text"));
+ bindable.SetBinding(MockBindable.TextProperty, new Binding("Text"));
- Assert.AreEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty));
+ Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty));
var dvm = new DifferentViewModel();
bindable.BindingContext = dvm;
- Assert.AreEqual (dvm.Text, bindable.GetValue (MockBindable.TextProperty));
- }
-
- [Test]
- public void PropertyChangeBindingsOccurThroughMainThread()
- {
- var vm = new MockViewModel { Text = "text" };
-
- var bindable = new MockBindable();
- bindable.BindingContext = vm;
- bindable.SetBinding (MockBindable.TextProperty, new Binding ("Text"));
-
- bool mainThread = false;
- Device.PlatformServices = new MockPlatformServices (invokeOnMainThread: a => mainThread = true);
-
- vm.Text = "updated";
-
- Assert.IsTrue (mainThread, "Binding did not occur on main thread");
- Assert.AreNotEqual (vm.Text, bindable.GetValue (MockBindable.TextProperty), "Binding was applied anyway through other means");
+ Assert.AreEqual(dvm.Text, bindable.GetValue(MockBindable.TextProperty));
}
[Test]
public void Clone()
{
object param = new object();
- var binding = new Binding (".", converter: new TestConverter<string, int>(), converterParameter: param, stringFormat: "{0}");
+ var binding = new Binding(".", converter: new TestConverter<string, int>(), converterParameter: param, stringFormat: "{0}");
var clone = (Binding)binding.Clone();
- Assert.AreSame (binding.Converter, clone.Converter);
- Assert.AreSame (binding.ConverterParameter, clone.ConverterParameter);
- Assert.AreEqual (binding.Mode, clone.Mode);
- Assert.AreEqual (binding.Path, clone.Path);
- Assert.AreEqual (binding.StringFormat, clone.StringFormat);
+ Assert.AreSame(binding.Converter, clone.Converter);
+ Assert.AreSame(binding.ConverterParameter, clone.ConverterParameter);
+ Assert.AreEqual(binding.Mode, clone.Mode);
+ Assert.AreEqual(binding.Path, clone.Path);
+ Assert.AreEqual(binding.StringFormat, clone.StringFormat);
}
[Test]
@@ -2247,7 +1688,7 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.That (log.Messages.Count, Is.EqualTo (1), "An error was not logged");
Assert.That (log.Messages[0], Is.StringContaining (String.Format (BindingExpression.PropertyNotFoundErrorMessage,
"MissingProperty",
- "Xamarin.Forms.Core.UnitTests.BindingUnitTests+ComplexMockViewModel",
+ "Xamarin.Forms.Core.UnitTests.BindingBaseUnitTests+ComplexMockViewModel",
"Xamarin.Forms.Core.UnitTests.MockBindable",
"Text")));
@@ -2673,4 +2114,4 @@ namespace Xamarin.Forms.Core.UnitTests
Assert.AreEqual ("Baz", label.Text);
}
}
-}
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/MockViewModel.cs b/Xamarin.Forms.Core.UnitTests/MockViewModel.cs
index da2f6f96..bdb3f7ef 100644
--- a/Xamarin.Forms.Core.UnitTests/MockViewModel.cs
+++ b/Xamarin.Forms.Core.UnitTests/MockViewModel.cs
@@ -3,28 +3,30 @@ using System.Runtime.CompilerServices;
namespace Xamarin.Forms.Core.UnitTests
{
- internal class MockViewModel
- : INotifyPropertyChanged
+ class MockViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
- string text;
+ public MockViewModel(string text=null)
+ {
+ _text = text;
+ }
+
+ string _text;
public virtual string Text {
- get { return text; }
+ get { return _text; }
set {
- if (text == value)
+ if (_text == value)
return;
- text = value;
+ _text = value;
OnPropertyChanged ("Text");
}
}
protected void OnPropertyChanged ([CallerMemberName] string propertyName = null)
{
- PropertyChangedEventHandler handler = PropertyChanged;
- if (handler != null)
- handler (this, new PropertyChangedEventArgs (propertyName));
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs (propertyName));
}
}
-}
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs
new file mode 100644
index 00000000..65a78276
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/TypedBindingUnitTests.cs
@@ -0,0 +1,1526 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NUnit.Framework;
+using System.Globalization;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using CategoryAttribute = NUnit.Framework.CategoryAttribute;
+using DescriptionAttribute = NUnit.Framework.DescriptionAttribute;
+using System.Threading.Tasks;
+using Xamarin.Forms.Internals;
+using System.Diagnostics;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+ [TestFixture]
+ public class TypedBindingUnitTests : BindingBaseUnitTests
+ {
+ [SetUp]
+ public override void Setup()
+ {
+ base.Setup();
+ log = new Logger();
+
+ Device.PlatformServices = new MockPlatformServices();
+ Log.Listeners.Add(log);
+ }
+
+ [TearDown]
+ public override void TearDown()
+ {
+ base.TearDown();
+ Device.PlatformServices = null;
+ Log.Listeners.Remove(log);
+ }
+
+ protected override BindingBase CreateBinding(BindingMode mode = BindingMode.Default, string stringFormat = null)
+ {
+ return new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ })
+ {
+ Mode = mode,
+ StringFormat= stringFormat
+ };
+ }
+
+ [Test]
+ public void InvalidCtor()
+ {
+ Assert.Throws<ArgumentNullException>(() => new TypedBinding<MockViewModel, string>(null, (mvm, s) => mvm.Text = s, null), "Allowed null getter");
+ }
+
+ [Test, NUnit.Framework.Category("[Binding] Set Value")]
+ public void ValueSetOnOneWayWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), null, propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ public void ValueSetOnOneWayToSourceWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), value, propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property changed");
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ public void ValueSetOnTwoWayWithComplexPathBinding(
+ [Values(true, false)] bool setContextFirst,
+ [Values(true, false)] bool isDefault)
+ {
+ const string value = "Foo";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = value
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ if (setContextFirst) {
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+ } else {
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = viewmodel;
+ }
+
+ Assert.AreEqual(value, viewmodel.Model.Model.Text,
+ "BindingContext property changed");
+ Assert.AreEqual(value, bindable.GetValue(property),
+ "Target property did not change");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnOneWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ string original = (string)bindable.GetValue(property);
+ const string value = "value";
+ viewmodel.Model.Model.Text = value;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource");
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithComplexPathOnTwoWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "Foo"
+ }
+ }
+ };
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model.Text,
+ (cmvm, s) => cmvm.Model.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Text")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model.Text = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model.Text,
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Model.Model.Text,
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnOneWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model[1],
+ (cmvm, s) => cmvm.Model.Model[1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model [1] = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ string original = (string)bindable.GetValue(property);
+ const string value = "value";
+ viewmodel.Model.Model [1] = value;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource");
+
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedPathOnTwoWayBinding(bool isDefault)
+ {
+ const string newvalue = "New Value";
+ var viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+ viewmodel.Model.Model [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ viewmodel.Model.Model [1] = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, viewmodel.Model.Model [1],
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Model.Model [1],
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Indexed paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithIndexedArrayPathOnTwoWayBinding(bool isDefault)
+ {
+ var viewmodel = new ComplexMockViewModel {
+ Array = new string [2]
+ };
+ viewmodel.Array [1] = "Foo";
+
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Array [1],
+ (cmvm, s) => cmvm.Array [1] = s,
+ null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, new Binding("Array[1]", bindingMode));
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, viewmodel.Array [1],
+ "Source property did not update with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithSelfPathOnOneWayBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm,
+ (cmvm, s) => cmvm = s,null){Mode = bindingMode};
+ const string value = "foo";
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = value;
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "value";
+ bindable.SetValue(property, newvalue);
+ Assert.AreEqual(value, bindable.BindingContext,
+ "Source was updated from Target on OneWay binding");
+
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Bindable did not update on binding context property change");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed when it shouldn't");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueDoesNotUpdateWithSelfPathOnOneWayToSourceBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.OneWayToSource;
+ if (isDefault) {
+ propertyDefault = BindingMode.OneWayToSource;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm, (cmvm, s) => cmvm = s, null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "new value";
+
+ string original = (string)bindable.GetValue(property);
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(original, bindable.GetValue(property),
+ "Target updated from Source on OneWayToSource with self path");
+
+ const string newvalue2 = "new value 2";
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed on OneWayToSource with self path");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed on OneWayToSource with self path");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Self paths")]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void ValueUpdatedWithSelfPathOnTwoWayBinding(bool isDefault)
+ {
+ BindingMode propertyDefault = BindingMode.OneWay;
+ BindingMode bindingMode = BindingMode.TwoWay;
+ if (isDefault) {
+ propertyDefault = BindingMode.TwoWay;
+ bindingMode = BindingMode.Default;
+ }
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value", propertyDefault);
+ var binding = new TypedBinding<string, string>(
+ cmvm => cmvm, (cmvm, s) => cmvm = s, null){Mode = bindingMode};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = "value";
+ bindable.SetBinding(property, binding);
+
+ const string newvalue = "New Value";
+ bindable.BindingContext = newvalue;
+ Assert.AreEqual(newvalue, bindable.GetValue(property),
+ "Target property did not update change");
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Source property changed from what it was set to");
+
+ const string newvalue2 = "New Value in the other direction";
+
+ bindable.SetValue(property, newvalue2);
+ Assert.AreEqual(newvalue, bindable.BindingContext,
+ "Self-path Source changed with Target's change");
+ Assert.AreEqual(newvalue2, bindable.GetValue(property),
+ "Target property changed from what it was set to");
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Category("[Binding] Complex paths")]
+ [TestCase(BindingMode.OneWay)]
+ [TestCase(BindingMode.OneWayToSource)]
+ [TestCase(BindingMode.TwoWay)]
+ public void SourceAndTargetAreWeakComplexPath(BindingMode mode)
+ {
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "default value");
+
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Model [1],
+ (cmvm, s) => cmvm.Model.Model [1] = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model.Model, "Indexer[1]")
+ }){Mode = mode};
+
+ WeakReference weakViewModel = null, weakBindable = null;
+
+ int i=0;
+ Action create = null;
+ create = ()=>{
+ if (i++ < 1024){
+ create();
+ return;
+ }
+ MockBindable bindable = new MockBindable();
+
+ weakBindable = new WeakReference(bindable);
+
+ ComplexMockViewModel viewmodel = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ }
+ };
+
+ weakViewModel = new WeakReference(viewmodel);
+
+ bindable.BindingContext = viewmodel;
+ bindable.SetBinding(property, binding);
+
+ bindable.BindingContext = null;
+ };
+
+ create();
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWay)
+ Assert.IsFalse(weakViewModel.IsAlive, "ViewModel wasn't collected");
+
+ if (mode == BindingMode.TwoWay || mode == BindingMode.OneWayToSource)
+ Assert.IsFalse(weakBindable.IsAlive, "Bindable wasn't collected");
+ }
+
+ class TestConverter<TSource, TTarget> : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Assert.AreEqual(typeof(TTarget), targetType);
+ return System.Convert.ChangeType(value, targetType, CultureInfo.CurrentUICulture);
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ Assert.AreEqual(typeof(TSource), targetType);
+ return System.Convert.ChangeType(value, targetType, CultureInfo.CurrentUICulture);
+ }
+ }
+
+ [Test]
+ public void ValueConverter()
+ {
+ var converter = new TestConverter<string, int>();
+
+ var vm = new MockViewModel("1");
+ var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 0);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, bindable.GetValue(property));
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ public void ValueConverterBack()
+ {
+ var converter = new TestConverter<string, int>();
+
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("TargetInt", typeof(int), typeof(MockBindable), 1, BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("1", vm.Text);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ class TestConverterParameter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return parameter;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return parameter;
+ }
+ }
+
+ [Test]
+ public void ValueConverterParameter()
+ {
+ var converter = new TestConverterParameter();
+
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "Bar", BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter, ConverterParameter = "Foo"};
+
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("Foo", vm.Text);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ class TestConverterCulture : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return culture.ToString();
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ return culture.ToString();
+ }
+ }
+
+#if !WINDOWS_PHONE
+ [Test]
+ [SetUICulture("pt-PT")]
+ public void ValueConverterCulture()
+ {
+ var converter = new TestConverterCulture();
+ var vm = new MockViewModel();
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "Bar", BindingMode.OneWayToSource);
+ var binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ }){Converter = converter};
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual("pt-PT", vm.Text);
+ }
+#endif
+
+ [Test]
+ public void SelfBindingConverter()
+ {
+ var converter = new TestConverter<int, string>();
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "0");
+ var binding = new TypedBinding<int, int>(
+ mvm => mvm, (mvm, s) => mvm = s, null){Converter = converter};
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = 1;
+ bindable.SetBinding(property, binding);
+ Assert.AreEqual("1", bindable.GetValue(property));
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ internal class MultiplePropertyViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ int done;
+ public int Done {
+ get { return done; }
+ set {
+ done = value;
+ OnPropertyChanged();
+ OnPropertyChanged("Progress");
+ }
+ }
+
+ int total = 100;
+ public int Total {
+ get { return total; }
+ set {
+ if (total == value)
+ return;
+
+ total = value;
+ OnPropertyChanged();
+ OnPropertyChanged("Progress");
+ }
+ }
+
+ public float Progress {
+ get { return (float)done / total; }
+ }
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChangedEventHandler handler = PropertyChanged;
+ if (handler != null)
+ handler(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ internal class MultiplePropertyBindable
+ : BindableObject
+ {
+ public static readonly BindableProperty ValueProperty = BindableProperty.Create ("Value", typeof(float), typeof(MultiplePropertyBindable), 0f);
+
+ public float Value {
+ get { return (float)GetValue(ValueProperty); }
+ set { SetValue(ValueProperty, value); }
+ }
+
+ public static readonly BindableProperty DoneProperty = BindableProperty.Create("Done", typeof(int), typeof(MultiplePropertyBindable), 0);
+
+ public int Done {
+ get { return (int)GetValue(DoneProperty); }
+ set { SetValue(DoneProperty, value); }
+ }
+ }
+
+ [Test]
+ public void MultiplePropertyUpdates()
+ {
+ var mpvm = new MultiplePropertyViewModel();
+
+ var bindable = new MultiplePropertyBindable();
+ var progressBinding = new TypedBinding<MultiplePropertyViewModel, float>(vm => vm.Progress, null, new [] {
+ new Tuple<Func<MultiplePropertyViewModel, object>, string> (vm=>vm, "Progress"),
+ }){Mode = BindingMode.OneWay};
+ var doneBinding = new TypedBinding<MultiplePropertyViewModel, int>(vm => vm.Done, (vm,d)=>vm.Done=d, new [] {
+ new Tuple<Func<MultiplePropertyViewModel, object>, string> (vm=>vm, "Done"),
+ }){Mode = BindingMode.OneWayToSource};
+
+ bindable.SetBinding(MultiplePropertyBindable.ValueProperty, progressBinding);
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty, doneBinding);
+ bindable.BindingContext = mpvm;
+
+ bindable.Done = 5;
+
+ Assert.AreEqual(5, mpvm.Done);
+ Assert.AreEqual(0.05f, mpvm.Progress);
+ Assert.AreEqual(5, bindable.Done);
+ Assert.AreEqual(0.05f, bindable.Value);
+
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ [Description("When part of a complex path can not be evaluated during an update, bindables should return to their default value.")]
+ public void NullInPathUsesDefaultValue()
+ {
+ var vm = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel()
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "foo bar");
+
+ var bindable = new MockBindable();
+ var binding = new TypedBinding<ComplexMockViewModel, string>(cvm => cvm.Model.Text, (cvm, t) => cvm.Model.Text = t, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm.Model, "Text")
+ }){Mode = BindingMode.OneWay};
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ vm.Model = null;
+
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property));
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test, Category("[Binding] Complex paths")]
+ [Description("When part of a complex path can not be evaluated during an update, bindables should return to their default value.")]
+ public void NullContextUsesDefaultValue()
+ {
+ var vm = new ComplexMockViewModel {
+ Model = new ComplexMockViewModel {
+ Text = "vm value"
+ }
+ };
+
+ var property = BindableProperty.Create("Text", typeof(string), typeof(MockBindable), "foo bar");
+ var binding = new TypedBinding<ComplexMockViewModel, string>(cvm => cvm.Model.Text, (cvm, t) => cvm.Model.Text = t, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cvm=>cvm.Model, "Text")
+ }){Mode = BindingMode.OneWay};
+ var bindable = new MockBindable();
+ bindable.SetBinding(property, binding);
+ bindable.BindingContext = vm;
+
+ Assume.That(bindable.GetValue(property), Is.EqualTo(vm.Model.Text));
+
+ bindable.BindingContext = null;
+
+ Assert.AreEqual(property.DefaultValue, bindable.GetValue(property));
+ Assert.That(log.Messages.Count, Is.EqualTo(0),
+ "An error was logged: " + log.Messages.FirstOrDefault());
+ }
+
+ [Test]
+ [Description("OneWay bindings should not double apply on source updates.")]
+ public void OneWayBindingsDontDoubleApplyOnSourceUpdates()
+ {
+ var vm = new ComplexMockViewModel();
+
+ var bindable = new MockBindable();
+ var binding = new TypedBinding<ComplexMockViewModel, int>(cmvm => cmvm.QueryCount, null, null){Mode = BindingMode.OneWay};
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty,binding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, vm.count);
+
+ bindable.BindingContext = null;
+
+ Assert.AreEqual(1, vm.count, "Source property was queried on an unset");
+
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(2, vm.count, "Source property was queried multiple times on a reapply");
+ }
+
+ [Test]
+ [Description("When there are multiple bindings, an update in one should not cause the other to udpate.")]
+ public void BindingsShouldNotTriggerOtherBindings()
+ {
+ var vm = new ComplexMockViewModel();
+
+ var bindable = new MockBindable();
+ var qcbinding = new TypedBinding<ComplexMockViewModel, int>(cmvm => cmvm.QueryCount, null, null){Mode = BindingMode.OneWay};
+ var textBinding = new TypedBinding<ComplexMockViewModel, string>(cmvm => cmvm.Text, null, null){Mode = BindingMode.OneWay};
+ bindable.SetBinding(MultiplePropertyBindable.DoneProperty, qcbinding);
+ bindable.SetBinding(MockBindable.TextProperty, textBinding);
+ bindable.BindingContext = vm;
+
+ Assert.AreEqual(1, vm.count);
+
+ vm.Text = "update";
+
+ Assert.AreEqual(1, vm.count, "Source property was queried due to a different binding update.");
+ }
+
+ internal class DerivedViewModel
+ : MockViewModel
+ {
+ public override string Text {
+ get { return base.Text + "2"; }
+ set { base.Text = value; }
+ }
+ }
+
+ [Test]
+ [Description("The most derived version of a property should always be called.")]
+ public void MostDerviedPropertyOnContextSwitchOfSimilarType()
+ {
+ var vm = new MockViewModel { Text = "text" };
+
+ var bindable = new MockBindable();
+ bindable.BindingContext = vm;
+ var binding = new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, new [] {
+ new Tuple<Func<MockViewModel, object>, string>(mvm=>mvm, "Text")
+ });
+ bindable.SetBinding(MockBindable.TextProperty, binding);
+
+ Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty));
+
+ bindable.BindingContext = vm = new DerivedViewModel { Text = "text" };
+
+ Assert.AreEqual(vm.Text, bindable.GetValue(MockBindable.TextProperty));
+ }
+
+ [Test]
+ [Description("When binding with a multi-part path and part is null, no error should be thrown or logged")]
+ public void ChainedPartNull()
+ {
+ var bindable = new MockBindable { BindingContext = new ComplexMockViewModel() };
+ var binding = new TypedBinding<ComplexMockViewModel, string>(
+ cmvm => cmvm.Model.Text,
+ (cmvm, s) => cmvm.Model.Text = s, new [] {
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm, "Model"),
+ new Tuple<Func<ComplexMockViewModel, object>, string>(cmvm=>cmvm.Model, "Text"),
+ });
+
+ Assert.That(() => bindable.SetBinding(MockBindable.TextProperty, binding), Throws.Nothing);
+ Assert.That(log.Messages.Count, Is.EqualTo(0), "An error was logged");
+ }
+
+ [Test]
+ public void SetBindingContextBeforeContextBindingAndInnerBindings()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAndInnerBindingBeforeContextBinding()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAfterContextBindingAndInnerBindings()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void SetBindingContextAfterInnerBindingsAndContextBinding()
+ {
+ var label = new Label();
+ var view = new StackLayout { Children = { label } };
+ var bindingItem1 = new TypedBinding<Tuple<string, string>, string>(s => s.Item1, null, null);
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+
+ label.SetBinding(Label.TextProperty, bindingItem1);
+ label.SetBinding(BindableObject.BindingContextProperty, bindingItem1);
+ view.BindingContext = new Tuple<string, string>("Foo", "Bar");
+
+ Assert.AreEqual("Foo", label.Text);
+ }
+
+ [Test]
+ public void Convert()
+ {
+ var slider = new Slider();
+ var vm = new MockViewModel { Text = "0.5" };
+ slider.BindingContext = vm;
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, null){Mode = BindingMode.TwoWay});
+
+ Assert.That(slider.Value, Is.EqualTo(0.5));
+
+ slider.Value = 0.9;
+
+ Assert.That(vm.Text, Is.EqualTo("0.9"));
+ }
+
+#if !WINDOWS_PHONE
+ [Test]
+ [SetCulture("pt-PT")]
+ [SetUICulture("pt-PT")]
+ public void ConvertIsCultureInvariant()
+ {
+ var slider = new Slider();
+ var vm = new MockViewModel { Text = "0.5" };
+ slider.BindingContext = vm;
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<MockViewModel, string>(mvm => mvm.Text, (mvm, s) => mvm.Text = s, null){Mode = BindingMode.TwoWay});
+
+ Assert.That(slider.Value, Is.EqualTo(0.5));
+
+ slider.Value = 0.9;
+
+ Assert.That(vm.Text, Is.EqualTo("0.9"));
+ }
+#endif
+
+ [Test]
+ public void FailToConvert()
+ {
+ var slider = new Slider();
+ slider.BindingContext = new ComplexMockViewModel { Model = new ComplexMockViewModel() };
+
+ Assert.That(() => {
+ slider.SetBinding(Slider.ValueProperty, new TypedBinding<ComplexMockViewModel, ComplexMockViewModel>(mvm => mvm.Model, null, null));
+ }, Throws.Nothing);
+
+ Assert.That(slider.Value, Is.EqualTo(Slider.ValueProperty.DefaultValue));
+ Assert.That(log.Messages.Count, Is.EqualTo(1), "No error logged");
+ }
+
+ class NullViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public string Foo {
+ get;
+ set;
+ }
+
+ public string Bar {
+ get;
+ set;
+ }
+
+ public void SignalAllPropertiesChanged(bool useNull)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs((useNull) ? null : String.Empty));
+ }
+ }
+
+ class MockBindable2 : MockBindable
+ {
+ public static readonly BindableProperty Text2Property = BindableProperty.Create("Text2", typeof(string), typeof(MockBindable2), "default", BindingMode.TwoWay);
+ public string Text2 {
+ get { return (string)GetValue(Text2Property); }
+ set { SetValue(Text2Property, value); }
+ }
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void NullPropertyUpdatesAllBindings(bool useStringEmpty)
+ {
+ var vm = new NullViewModel();
+ var bindable = new MockBindable2();
+ bindable.BindingContext = vm;
+ bindable.SetBinding(MockBindable.TextProperty, new TypedBinding<NullViewModel,string>(nvm => nvm.Foo, null, new [] {
+ new Tuple<Func<NullViewModel, object>, string>(nvm=>nvm,"Foo")
+ }));
+ bindable.SetBinding(MockBindable2.Text2Property, new TypedBinding<NullViewModel, string>(nvm => nvm.Bar, null, new [] {
+ new Tuple<Func<NullViewModel, object>, string>(nvm=>nvm,"Bar")
+ }));
+
+ vm.Foo = "Foo";
+ vm.Bar = "Bar";
+ Assert.That(() => vm.SignalAllPropertiesChanged(useNull: !useStringEmpty), Throws.Nothing);
+
+ Assert.That(bindable.Text, Is.EqualTo("Foo"));
+ Assert.That(bindable.Text2, Is.EqualTo("Bar"));
+ }
+
+ [TestCase]
+ public void BindingSourceOverContext()
+ {
+ var label = new Label();
+ label.BindingContext = "bindingcontext";
+ var bindingSelf = new TypedBinding<string, string>(s => s, null, null);
+ label.SetBinding(Label.TextProperty, bindingSelf);
+ Assert.AreEqual("bindingcontext", label.Text);
+
+ var bindingSelfSource= new TypedBinding<string, string>(s => s, null, null){Source = "bindingsource"};
+ label.SetBinding(Label.TextProperty, bindingSelfSource);
+ Assert.AreEqual("bindingsource", label.Text);
+ }
+
+ class TestViewModel : INotifyPropertyChanged
+ {
+ event PropertyChangedEventHandler PropertyChanged;
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged {
+ add { PropertyChanged += value; }
+ remove { PropertyChanged -= value; }
+ }
+
+ public string Foo { get; set; }
+
+ public int InvocationListSize()
+ {
+ if (PropertyChanged == null)
+ return 0;
+ return PropertyChanged.GetInvocationList().Length;
+ }
+
+ public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ [Test]
+ public void BindingUnsubscribesForDeadTarget()
+ {
+ var viewmodel = new TestViewModel();
+
+ int i = 0;
+ Action create = null;
+ create = () => {
+ if (i++ < 1024) {
+ create();
+ return;
+ }
+
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, new TypedBinding<TestViewModel,string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ }));
+ button.BindingContext = viewmodel;
+ };
+
+ create();
+
+ Assume.That(viewmodel.InvocationListSize(), Is.EqualTo(1));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ viewmodel.OnPropertyChanged("Foo");
+
+ Assert.AreEqual(0, viewmodel.InvocationListSize());
+ }
+
+ [Test]
+ public void BindingDoesNotStayAliveForDeadTarget()
+ {
+ var viewModel = new TestViewModel();
+ WeakReference bindingRef = null;
+
+ int i = 0;
+ Action create = null;
+ create = () => {
+ if (i++ < 1024) {
+ create();
+ return;
+ }
+
+ var binding = new TypedBinding<TestViewModel, string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ });
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, binding);
+ button.BindingContext = viewModel;
+
+ bindingRef = new WeakReference(binding);
+ binding = null;
+ };
+
+ create();
+
+ Assume.That(viewModel.InvocationListSize(), Is.EqualTo(1));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ Assert.IsFalse(bindingRef.IsAlive, "Binding should not be alive!");
+ }
+
+ [Test]
+ public void BindingCreatesSingleSubscription()
+ {
+ TestViewModel viewmodel = new TestViewModel();
+ var binding = new TypedBinding<TestViewModel, string>(vm => vm.Foo, (vm, s) => vm.Foo = s, new [] {
+ new Tuple<Func<TestViewModel, object>, string>(vm=>vm,"Foo")
+ });
+
+ var button = new Button();
+ button.SetBinding(Button.TextProperty, binding);
+ button.BindingContext = viewmodel;
+
+ Assert.That(viewmodel.InvocationListSize(), Is.EqualTo(1));
+ }
+
+ public class IndexedViewModel : INotifyPropertyChanged
+ {
+ Dictionary<string, object> dict = new Dictionary<string, object>();
+
+ [IndexerName("Item")]
+ public object this [string index] {
+ get { return dict [index]; }
+ set {
+ dict [index] = value;
+ OnPropertyChanged($"Item[{index}]");
+ }
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+
+ [Test]
+ public void IndexedViewModelPropertyChanged()
+ {
+ var label = new Label();
+ var viewModel = new IndexedViewModel();
+
+ var binding = new TypedBinding<Tuple<IndexedViewModel, object>, object>(
+ vm => vm.Item1["Foo"],
+ (vm, s) => vm.Item1 ["Foo"] = s,
+ new [] {
+ new Tuple<Func<Tuple<IndexedViewModel, object>, object>, string>(vm=>vm, "Item1"),
+ new Tuple<Func<Tuple<IndexedViewModel, object>, object>, string>(vm=>vm.Item1, "Item[Foo]"),
+ });
+
+ label.BindingContext = new Tuple<IndexedViewModel, object>(viewModel, new object());
+ label.SetBinding(Label.TextProperty, binding);
+ Assert.AreEqual(null, label.Text);
+
+ viewModel ["Foo"] = "Baz";
+
+ Assert.AreEqual("Baz", label.Text);
+ }
+
+ [Test]
+ [Ignore]
+ public void SpeedTestApply()
+ {
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var vm0 = new MockViewModel { Text = "Foo" };
+ var vm1 = new MockViewModel { Text = "Bar" };
+ var bindable = new MockBindable();
+
+ var it = 100000;
+
+ BindingBase binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ });
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swtb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swtb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: null);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swtbh = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swtbh.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new Binding("Text");
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++) {
+ binding.Apply(i % 2 == 0 ? vm0 : vm1, bindable, property);
+ binding.Unapply();
+ }
+ swb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ var swsv = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.SetValue(property, (i % 2 == 0 ? vm0 : vm1).Text);
+ swsv.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ Assert.Fail($"Applying {it} Typedbindings\t\t\t: {swtb.ElapsedMilliseconds}ms.\nApplying {it} Typedbindings (without INPC)\t: {swtbh.ElapsedMilliseconds}ms.\nApplying {it} Bindings\t\t\t: {swb.ElapsedMilliseconds}ms.\nSetting {it} values\t\t\t\t: {swsv.ElapsedMilliseconds}ms.");
+ }
+
+ [Test]
+ [Ignore]
+ public void SpeedTestSetBC()
+ {
+
+ var property = BindableProperty.Create("Foo", typeof(string), typeof(MockBindable));
+ var vm0 = new MockViewModel { Text = "Foo" };
+ var vm1 = new MockViewModel { Text = "Bar" };
+ var bindable = new MockBindable();
+
+ var it = 100000;
+
+ BindingBase binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: new [] {
+ new Tuple<Func<MockViewModel, object>, string> (mvm=>mvm, "Text")
+ });
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swtb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swtb.Stop();
+ //Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new TypedBinding<MockViewModel, string>(
+ getter: mvm => mvm.Text,
+ setter: (mvm, s) => mvm.Text = s,
+ handlers: null);
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swtbh = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swtbh.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ binding = new Binding("Text");
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swb = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.BindingContext = i % 2 == 0 ? vm0 : vm1;
+ swb.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ bindable.SetBinding(property, binding);
+ var swsv = Stopwatch.StartNew();
+ for (var i = 0; i < it; i++)
+ bindable.SetValue(property, (i % 2 == 0 ? vm0 : vm1).Text);
+ swsv.Stop();
+ Assert.AreEqual("Bar", bindable.GetValue(property));
+
+ Assert.Fail($"Setting BC for {it} Typedbindings\t\t\t: {swtb.ElapsedMilliseconds}ms.\nSetting BC for {it} Typedbindings (without INPC)\t: {swtbh.ElapsedMilliseconds}ms.\nSetting BC for {it} Bindings\t\t\t\t: {swb.ElapsedMilliseconds}ms.\nSetting {it} values\t\t\t\t\t: {swsv.ElapsedMilliseconds}ms.");
+ }
+ }
+}
diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
index e4a36d1a..243cbce3 100644
--- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
+++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
@@ -180,6 +180,7 @@
<Compile Include="PinchGestureRecognizerTests.cs" />
<Compile Include="AppLinkEntryTests.cs" />
<Compile Include="NativeBindingTests.cs" />
+ <Compile Include="TypedBindingUnitTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Xamarin.Forms.Core\Xamarin.Forms.Core.csproj">
diff --git a/Xamarin.Forms.Core/BindableObject.cs b/Xamarin.Forms.Core/BindableObject.cs
index f3482004..fdf2afad 100644
--- a/Xamarin.Forms.Core/BindableObject.cs
+++ b/Xamarin.Forms.Core/BindableObject.cs
@@ -9,13 +9,13 @@ namespace Xamarin.Forms
{
public abstract class BindableObject : INotifyPropertyChanged, IDynamicResourceHandler
{
- public static readonly BindableProperty BindingContextProperty = BindableProperty.Create("BindingContext", typeof(object), typeof(BindableObject), default(object), BindingMode.OneWay, null,
- BindingContextPropertyBindingPropertyChanged, null, null, BindingContextPropertyBindingChanging);
+ public static readonly BindableProperty BindingContextProperty =
+ BindableProperty.Create("BindingContext", typeof(object), typeof(BindableObject), default(object),
+ BindingMode.OneWay, null, BindingContextPropertyChanged, null, null, BindingContextPropertyBindingChanging);
readonly List<BindablePropertyContext> _properties = new List<BindablePropertyContext>(4);
bool _applying;
-
object _inheritedContext;
public object BindingContext
@@ -114,20 +114,18 @@ namespace Xamarin.Forms
bindable._inheritedContext = value;
}
- bindable.ApplyBindings(oldContext);
+ bindable.ApplyBindings();
bindable.OnBindingContextChanged();
}
- protected void ApplyBindings(object oldContext = null)
+ protected void ApplyBindings()
{
- ApplyBindings(oldContext, false);
+ ApplyBindings(false);
}
protected virtual void OnBindingContextChanged()
{
- EventHandler change = BindingContextChanged;
- if (change != null)
- change(this, EventArgs.Empty);
+ BindingContextChanged?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
@@ -146,8 +144,8 @@ namespace Xamarin.Forms
protected void UnapplyBindings()
{
- foreach (BindablePropertyContext context in _properties)
- {
+ for (int i = 0, _propertiesCount = _properties.Count; i < _propertiesCount; i++) {
+ BindablePropertyContext context = _properties [i];
if (context.Binding == null)
continue;
@@ -393,15 +391,16 @@ namespace Xamarin.Forms
}
}
- void ApplyBindings(object oldContext, bool skipBindingContext)
+ void ApplyBindings(bool skipBindingContext)
{
- foreach (BindablePropertyContext context in _properties.ToArray())
- {
+ var prop = _properties.ToArray();
+ for (int i = 0, propLength = prop.Length; i < propLength; i++) {
+ BindablePropertyContext context = prop [i];
BindingBase binding = context.Binding;
if (binding == null)
continue;
- if (skipBindingContext && context.Property == BindingContextProperty)
+ if (skipBindingContext && ReferenceEquals(context.Property, BindingContextProperty))
continue;
binding.Unapply();
@@ -421,11 +420,10 @@ namespace Xamarin.Forms
newBinding.Context = context;
}
- static void BindingContextPropertyBindingPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
+ static void BindingContextPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
{
- object oldInheritedContext = bindable._inheritedContext;
bindable._inheritedContext = null;
- bindable.ApplyBindings(oldInheritedContext ?? oldvalue, true);
+ bindable.ApplyBindings(true);
bindable.OnBindingContextChanged();
}
diff --git a/Xamarin.Forms.Core/BindableObjectExtensions.cs b/Xamarin.Forms.Core/BindableObjectExtensions.cs
index 2eab2380..fec2ad88 100644
--- a/Xamarin.Forms.Core/BindableObjectExtensions.cs
+++ b/Xamarin.Forms.Core/BindableObjectExtensions.cs
@@ -17,6 +17,7 @@ namespace Xamarin.Forms
self.SetBinding(targetProperty, binding);
}
+ [Obsolete]
public static void SetBinding<TSource>(this BindableObject self, BindableProperty targetProperty, Expression<Func<TSource, object>> sourceProperty, BindingMode mode = BindingMode.Default,
IValueConverter converter = null, string stringFormat = null)
{
diff --git a/Xamarin.Forms.Core/Binding.cs b/Xamarin.Forms.Core/Binding.cs
index b3baa0df..71a2996b 100644
--- a/Xamarin.Forms.Core/Binding.cs
+++ b/Xamarin.Forms.Core/Binding.cs
@@ -90,6 +90,7 @@ namespace Xamarin.Forms
}
}
+ [Obsolete]
public static Binding Create<TSource>(Expression<Func<TSource, object>> propertyGetter, BindingMode mode = BindingMode.Default, IValueConverter converter = null, object converterParameter = null,
string stringFormat = null)
{
@@ -151,6 +152,7 @@ namespace Xamarin.Forms
_expression.Unapply();
}
+ [Obsolete]
static string GetBindingPath<TSource>(Expression<Func<TSource, object>> propertyGetter)
{
Expression expr = propertyGetter.Body;
diff --git a/Xamarin.Forms.Core/BindingBase.cs b/Xamarin.Forms.Core/BindingBase.cs
index 0810cbcf..fd11ac64 100644
--- a/Xamarin.Forms.Core/BindingBase.cs
+++ b/Xamarin.Forms.Core/BindingBase.cs
@@ -49,7 +49,7 @@ namespace Xamarin.Forms
public static void DisableCollectionSynchronization(IEnumerable collection)
{
if (collection == null)
- throw new ArgumentNullException("collection");
+ throw new ArgumentNullException(nameof(collection));
SynchronizedCollections.Remove(collection);
}
@@ -57,9 +57,9 @@ namespace Xamarin.Forms
public static void EnableCollectionSynchronization(IEnumerable collection, object context, CollectionSynchronizationCallback callback)
{
if (collection == null)
- throw new ArgumentNullException("collection");
+ throw new ArgumentNullException(nameof(collection));
if (callback == null)
- throw new ArgumentNullException("callback");
+ throw new ArgumentNullException(nameof(callback));
SynchronizedCollections.Add(collection, new CollectionSynchronizationContext(context, callback));
}
@@ -98,7 +98,7 @@ namespace Xamarin.Forms
internal static bool TryGetSynchronizedCollection(IEnumerable collection, out CollectionSynchronizationContext synchronizationContext)
{
if (collection == null)
- throw new ArgumentNullException("collection");
+ throw new ArgumentNullException(nameof(collection));
return SynchronizedCollections.TryGetValue(collection, out synchronizationContext);
}
diff --git a/Xamarin.Forms.Core/BindingBaseExtensions.cs b/Xamarin.Forms.Core/BindingBaseExtensions.cs
index a52c5cb1..5da40f23 100644
--- a/Xamarin.Forms.Core/BindingBaseExtensions.cs
+++ b/Xamarin.Forms.Core/BindingBaseExtensions.cs
@@ -1,16 +1,9 @@
-using System;
-
-namespace Xamarin.Forms
+namespace Xamarin.Forms
{
- internal static class BindingBaseExtensions
+ static class BindingBaseExtensions
{
- internal static BindingMode GetRealizedMode(this BindingBase self, BindableProperty property)
+ public static BindingMode GetRealizedMode(this BindingBase self, BindableProperty property)
{
- if (self == null)
- throw new ArgumentNullException("self");
- if (property == null)
- throw new ArgumentNullException("property");
-
return self.Mode != BindingMode.Default ? self.Mode : property.DefaultBindingMode;
}
}
diff --git a/Xamarin.Forms.Core/BindingExpression.cs b/Xamarin.Forms.Core/BindingExpression.cs
index 71ba0512..204b171d 100644
--- a/Xamarin.Forms.Core/BindingExpression.cs
+++ b/Xamarin.Forms.Core/BindingExpression.cs
@@ -406,50 +406,46 @@ namespace Xamarin.Forms
public object Source { get; private set; }
}
- class WeakPropertyChangedProxy
+ internal class WeakPropertyChangedProxy
{
- WeakReference _source, _listener;
- internal WeakReference Source => _source;
+ readonly WeakReference<INotifyPropertyChanged> _source = new WeakReference<INotifyPropertyChanged>(null);
+ readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null);
+ readonly PropertyChangedEventHandler _handler;
+ internal WeakReference<INotifyPropertyChanged> Source => _source;
- public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
+ public WeakPropertyChangedProxy()
{
- source.PropertyChanged += OnPropertyChanged;
- _source = new WeakReference(source);
- _listener = new WeakReference(listener);
+ _handler = new PropertyChangedEventHandler(OnPropertyChanged);
+ }
+
+ public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this()
+ {
+ SubscribeTo(source, listener);
+ }
+
+ public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
+ {
+ source.PropertyChanged += _handler;
+ _source.SetTarget(source);
+ _listener.SetTarget(listener);
}
public void Unsubscribe()
{
- if (_source != null)
- {
- var source = _source.Target as INotifyPropertyChanged;
- if (source != null)
- {
- source.PropertyChanged -= OnPropertyChanged;
- }
- _source = null;
- _listener = null;
- }
+ INotifyPropertyChanged source;
+ if (_source.TryGetTarget(out source) && source!=null)
+ source.PropertyChanged -= _handler;
+ _source.SetTarget(null);
+ _listener.SetTarget(null);
}
- private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
- if (_listener != null)
- {
- var handler = _listener.Target as PropertyChangedEventHandler;
- if (handler != null)
- {
- handler(sender, e);
- }
- else
- {
- Unsubscribe();
- }
- }
+ PropertyChangedEventHandler handler;
+ if (_listener.TryGetTarget(out handler) && handler != null)
+ handler(sender, e);
else
- {
Unsubscribe();
- }
}
}
@@ -471,7 +467,8 @@ namespace Xamarin.Forms
public void Subscribe(INotifyPropertyChanged handler)
{
- if (ReferenceEquals(handler, _listener?.Source?.Target))
+ INotifyPropertyChanged source;
+ if (_listener != null && _listener.Source.TryGetTarget(out source) && ReferenceEquals(handler, source))
{
// Already subscribed
return;
diff --git a/Xamarin.Forms.Core/TypedBinding.cs b/Xamarin.Forms.Core/TypedBinding.cs
new file mode 100644
index 00000000..c98b39df
--- /dev/null
+++ b/Xamarin.Forms.Core/TypedBinding.cs
@@ -0,0 +1,291 @@
+#define DO_NOT_CHECK_FOR_BINDING_REUSE
+
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Collections.Generic;
+
+namespace Xamarin.Forms.Internals
+{
+ //FIXME: need a better name for this, and share with Binding, so we can share more unittests
+ public abstract class TypedBindingBase : BindingBase
+ {
+ IValueConverter _converter;
+ object _converterParameter;
+ object _source;
+ string _updateSourceEventName;
+
+ public IValueConverter Converter {
+ get { return _converter; }
+ set {
+ ThrowIfApplied();
+ _converter = value;
+ }
+ }
+
+ public object ConverterParameter {
+ get { return _converterParameter; }
+ set {
+ ThrowIfApplied();
+ _converterParameter = value;
+ }
+ }
+
+ public object Source {
+ get { return _source; }
+ set {
+ ThrowIfApplied();
+ _source = value;
+ }
+ }
+
+ internal string UpdateSourceEventName {
+ get { return _updateSourceEventName; }
+ set {
+ ThrowIfApplied();
+ _updateSourceEventName = value;
+ }
+ }
+
+ internal TypedBindingBase()
+ {
+ }
+ }
+
+ public sealed class TypedBinding<TSource, TProperty> : TypedBindingBase
+ {
+ readonly Func<TSource, TProperty> _getter;
+ readonly Action<TSource, TProperty> _setter;
+ readonly PropertyChangedProxy [] _handlers;
+
+ public TypedBinding(Func<TSource, TProperty> getter, Action<TSource, TProperty> setter, Tuple<Func<TSource, object>, string> [] handlers)
+ {
+ if (getter == null)
+ throw new ArgumentNullException(nameof(getter));
+
+ _getter = getter;
+ _setter = setter;
+
+ if (handlers == null)
+ return;
+
+ _handlers = new PropertyChangedProxy [handlers.Length];
+ for (var i = 0; i < handlers.Length; i++)
+ _handlers [i] = new PropertyChangedProxy(handlers [i].Item1, handlers [i].Item2, this);
+ }
+
+ readonly WeakReference<object> _weakSource = new WeakReference<object>(null);
+ readonly WeakReference<BindableObject> _weakTarget = new WeakReference<BindableObject>(null);
+ BindableProperty _targetProperty;
+
+ // Applies the binding to a previously set source and target.
+ internal override void Apply(bool fromTarget = false)
+ {
+ base.Apply(fromTarget);
+
+ BindableObject target;
+#if DO_NOT_CHECK_FOR_BINDING_REUSE
+ if (!_weakTarget.TryGetTarget(out target))
+ throw new InvalidOperationException();
+#else
+ if (!_weakTarget.TryGetTarget(out target) || target == null) {
+ Unapply();
+ return;
+ }
+#endif
+ object source;
+ if (_weakSource.TryGetTarget(out source) && source != null)
+ ApplyCore(source, target, _targetProperty, fromTarget);
+ }
+
+ // Applies the binding to a new source or target.
+ internal override void Apply(object context, BindableObject bindObj, BindableProperty targetProperty)
+ {
+ _targetProperty = targetProperty;
+ var source = Source ?? Context ?? context;
+
+#if (!DO_NOT_CHECK_FOR_BINDING_REUSE)
+ base.Apply(source, bindObj, targetProperty);
+
+ BindableObject prevTarget;
+ if (_weakTarget.TryGetTarget(out prevTarget) && !ReferenceEquals(prevTarget, bindObj))
+ throw new InvalidOperationException("Binding instances can not be reused");
+
+ object previousSource;
+ if (_weakSource.TryGetTarget(out previousSource) && !ReferenceEquals(previousSource, source))
+ throw new InvalidOperationException("Binding instances can not be reused");
+#endif
+ _weakSource.SetTarget(source);
+ _weakTarget.SetTarget(bindObj);
+
+ ApplyCore(source, bindObj, targetProperty);
+ }
+
+ internal override BindingBase Clone()
+ {
+ Tuple<Func<TSource, object>, string> [] handlers = _handlers == null ? null : new Tuple<Func<TSource, object>, string> [_handlers.Length];
+ if (handlers != null) {
+ for (var i = 0; i < _handlers.Length; i++)
+ handlers [i] = new Tuple<Func<TSource, object>, string>(_handlers [i].PartGetter, _handlers [i].PropertyName);
+ }
+ return new TypedBinding<TSource, TProperty>(_getter, _setter, handlers) {
+ Mode = Mode,
+ Converter = Converter,
+ ConverterParameter = ConverterParameter,
+ StringFormat = StringFormat,
+ Source = Source,
+ UpdateSourceEventName = UpdateSourceEventName,
+ };
+ }
+
+ internal override object GetSourceValue(object value, Type targetPropertyType)
+ {
+ if (Converter != null)
+ value = Converter.Convert(value, targetPropertyType, ConverterParameter, CultureInfo.CurrentUICulture);
+
+ //return base.GetSourceValue(value, targetPropertyType);
+ if (StringFormat != null)
+ return string.Format(StringFormat, value);
+
+ return value;
+ }
+
+ internal override object GetTargetValue(object value, Type sourcePropertyType)
+ {
+ if (Converter != null)
+ value = Converter.ConvertBack(value, sourcePropertyType, ConverterParameter, CultureInfo.CurrentUICulture);
+
+ //return base.GetTargetValue(value, sourcePropertyType);
+ return value;
+ }
+
+ internal override void Unapply()
+ {
+#if (!DO_NOT_CHECK_FOR_BINDING_REUSE)
+ base.Unapply();
+#endif
+ if (_handlers != null)
+ Unsubscribe();
+
+#if (!DO_NOT_CHECK_FOR_BINDING_REUSE)
+ _weakSource.SetTarget(null);
+ _weakTarget.SetTarget(null);
+#endif
+ }
+
+ // ApplyCore is as slim as it should be:
+ // Setting 100000 values : 17ms.
+ // ApplyCore 100000 (w/o INPC, w/o unnapply) : 20ms.
+ internal void ApplyCore(object sourceObject, BindableObject target, BindableProperty property, bool fromTarget = false)
+ {
+ var isTSource = sourceObject != null && sourceObject is TSource;
+ var mode = this.GetRealizedMode(property);
+ if (mode == BindingMode.OneWay && fromTarget)
+ return;
+
+ var needsGetter = (mode == BindingMode.TwoWay && !fromTarget) || mode == BindingMode.OneWay;
+
+ if (isTSource && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) && _handlers != null)
+ Subscribe((TSource)sourceObject);
+
+ if (needsGetter) {
+ var value = property.DefaultValue;
+ if (isTSource) {
+ try {
+ value = GetSourceValue(_getter((TSource)sourceObject), property.ReturnType);
+ } catch (Exception ex) when (ex is NullReferenceException || ex is KeyNotFoundException) {
+ }
+ }
+ if (!TryConvert(ref value, property, property.ReturnType, true)) {
+ Log.Warning("Binding", "{0} can not be converted to type '{1}'", value, property.ReturnType);
+ return;
+ }
+ target.SetValueCore(property, value, BindableObject.SetValueFlags.ClearDynamicResource, BindableObject.SetValuePrivateFlags.Default | BindableObject.SetValuePrivateFlags.Converted);
+ return;
+ }
+
+ var needsSetter = (mode == BindingMode.TwoWay && fromTarget) || mode == BindingMode.OneWayToSource;
+ if (needsSetter && _setter != null && isTSource) {
+ var value = GetTargetValue(target.GetValue(property), typeof(TProperty));
+ if (!TryConvert(ref value, property, typeof(TProperty), false)) {
+ Log.Warning("Binding", "{0} can not be converted to type '{1}'", value, typeof(TProperty));
+ return;
+ }
+ _setter((TSource)sourceObject, (TProperty)value);
+ }
+ }
+
+ static bool TryConvert(ref object value, BindableProperty targetProperty, Type convertTo, bool toTarget)
+ {
+ if (value == null)
+ return true;
+ if ((toTarget && targetProperty.TryConvert(ref value)) || (!toTarget && convertTo.IsInstanceOfType(value)))
+ return true;
+
+ object original = value;
+ try {
+ value = Convert.ChangeType(value, convertTo, CultureInfo.InvariantCulture);
+ return true;
+ } catch (Exception ex ) when (ex is InvalidCastException || ex is FormatException||ex is OverflowException) {
+ value = original;
+ return false;
+ }
+ }
+
+ class PropertyChangedProxy
+ {
+ public Func<TSource, object> PartGetter { get; }
+ public string PropertyName { get; }
+ public BindingExpression.WeakPropertyChangedProxy Listener { get; }
+ WeakReference<INotifyPropertyChanged> _weakPart = new WeakReference<INotifyPropertyChanged>(null);
+ readonly BindingBase _binding;
+
+ public INotifyPropertyChanged Part {
+ get {
+ INotifyPropertyChanged target;
+ if (_weakPart.TryGetTarget(out target))
+ return target;
+ return null;
+ }
+ set {
+ _weakPart.SetTarget(value);
+ Listener.SubscribeTo(value, OnPropertyChanged);
+ }
+ }
+
+ public PropertyChangedProxy(Func<TSource, object> partGetter, string propertyName, BindingBase binding)
+ {
+ PartGetter = partGetter;
+ PropertyName = propertyName;
+ _binding = binding;
+ Listener = new BindingExpression.WeakPropertyChangedProxy();
+ }
+
+ void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (!string.IsNullOrEmpty(e.PropertyName) && string.CompareOrdinal(e.PropertyName, PropertyName) != 0)
+ return;
+ Device.BeginInvokeOnMainThread(() => _binding.Apply(false));
+ }
+ }
+
+ void Subscribe(TSource sourceObject)
+ {
+ for (var i = 0; i < _handlers.Length; i++) {
+ var part = _handlers [i].PartGetter(sourceObject);
+ if (part == null)
+ break;
+ var inpc = part as INotifyPropertyChanged;
+ if (inpc == null)
+ continue;
+ _handlers [i].Part = (inpc);
+ }
+ }
+
+ void Unsubscribe()
+ {
+ for (var i = 0; i < _handlers.Length; i++)
+ _handlers [i].Listener.Unsubscribe();
+ }
+ }
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Core/WeakReferenceExtensions.cs b/Xamarin.Forms.Core/WeakReferenceExtensions.cs
index e4e883a4..a35331c4 100644
--- a/Xamarin.Forms.Core/WeakReferenceExtensions.cs
+++ b/Xamarin.Forms.Core/WeakReferenceExtensions.cs
@@ -2,12 +2,12 @@
namespace Xamarin.Forms
{
- internal static class WeakReferenceExtensions
+ static class WeakReferenceExtensions
{
internal static bool TryGetTarget<T>(this WeakReference self, out T target) where T : class
{
if (self == null)
- throw new ArgumentNullException("self");
+ throw new ArgumentNullException(nameof(self));
target = (T)self.Target;
return target != null;
diff --git a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
index 7de08e7e..48bed536 100644
--- a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
+++ b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
@@ -439,6 +439,7 @@
<Compile Include="INativeValueConverterService.cs" />
<Compile Include="INativeBindingService.cs" />
<Compile Include="ProvideCompiledAttribute.cs" />
+ <Compile Include="TypedBinding.cs" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<ItemGroup>
diff --git a/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml b/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml
new file mode 100644
index 00000000..4d3deeee
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ContentPage
+ xmlns="http://xamarin.com/schemas/2014/forms"
+ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
+ xmlns:local="clr-namespace:Xamarin.Forms.Xaml.UnitTests"
+ xmlns:sys="clr-namespace:System;assembly=mscorlib"
+
+ x:Class="Xamarin.Forms.Xaml.UnitTests.BindingsCompiler" >
+ <StackLayout>
+ <StackLayout x:DataType="local:MockViewModel">
+ <Label Text="{Binding Text}" x:Name="label0" />
+ <Label Text="{Binding Path=Text}" x:Name="label1" />
+ <Label Text="{Binding Model.Text}" x:Name="label2" />
+ <Label Text="{Binding Model[3]}" x:Name="label3" />
+ <Label Text="{Binding}" x:Name="label4" x:DataType="sys:String"/>
+ <Entry Text="{Binding Text, Mode=TwoWay}" x:Name="entry0"/>
+ </StackLayout>
+
+ <Label Text="{Binding Text}" x:Name="labelWithUncompiledBinding" />
+ </StackLayout>
+</ContentPage> \ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml.cs
new file mode 100644
index 00000000..843fe1c2
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/BindingsCompiler.xaml.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+
+using Xamarin.Forms;
+using NUnit.Framework;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using System.Diagnostics;
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+ public partial class BindingsCompiler : ContentPage
+ {
+ public BindingsCompiler()
+ {
+ InitializeComponent();
+ }
+
+ public BindingsCompiler(bool useCompiledXaml)
+ {
+ //this stub will be replaced at compile time
+ }
+
+ [TestFixture]
+ public class Tests
+ {
+ [SetUp]
+ public void Setup()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ Device.PlatformServices = null;
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void Test(bool useCompiledXaml)
+ {
+ var vm = new MockViewModel {
+ Text = "Text0",
+ Model = new MockViewModel {
+ Text = "Text1"
+ },
+ };
+ vm.Model [3] = "TextIndex";
+
+ var layout = new BindingsCompiler(useCompiledXaml);
+ layout.BindingContext = vm;
+ //testing paths
+ Assert.AreEqual("Text0", layout.label0.Text);
+ Assert.AreEqual("Text0", layout.label1.Text);
+ Assert.AreEqual("Text1", layout.label2.Text);
+ Assert.AreEqual("TextIndex", layout.label3.Text);
+
+ //testing selfPath
+ layout.label4.BindingContext = "Self";
+ Assert.AreEqual("Self", layout.label4.Text);
+
+ //testing INPC
+ vm.Text = "Text2";
+ Assert.AreEqual("Text2", layout.label0.Text);
+
+ //testing 2way
+ Assert.AreEqual("Text2", layout.entry0.Text);
+ ((IElementController)layout.entry0).SetValueFromRenderer(Entry.TextProperty, "Text3");
+ Assert.AreEqual("Text3", layout.entry0.Text);
+
+ //testing invalid bindingcontext type
+ layout.BindingContext = new object();
+ Assert.AreEqual(null, layout.label0.Text);
+ }
+ }
+ }
+
+ class MockViewModel : INotifyPropertyChanged
+ {
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ public MockViewModel(string text = null)
+ {
+ _text = text;
+ }
+
+ string _text;
+ public string Text {
+ get { return _text; }
+ set {
+ if (_text == value)
+ return;
+
+ _text = value;
+ OnPropertyChanged();
+ }
+ }
+
+ MockViewModel _model;
+ public MockViewModel Model {
+ get { return _model; }
+ set {
+ if (_model == value)
+ return;
+ _model = value;
+ OnPropertyChanged();
+ }
+ }
+
+ string [] values = new string [5];
+ [IndexerName("Indexer")]
+ public string this [int v] {
+ get { return values [v]; }
+ set {
+ if (values [v] == value)
+ return;
+
+ values [v] = value;
+ OnPropertyChanged("Indexer[" + v + "]");
+ }
+ }
+
+ protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+} \ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/MockCompiler.cs b/Xamarin.Forms.Xaml.UnitTests/MockCompiler.cs
index 404169b3..ccbfa20a 100644
--- a/Xamarin.Forms.Xaml.UnitTests/MockCompiler.cs
+++ b/Xamarin.Forms.Xaml.UnitTests/MockCompiler.cs
@@ -23,7 +23,7 @@ namespace Xamarin.Forms.Xaml.UnitTests
};
var exceptions = new List<Exception>();
- if (!xamlc.Compile(exceptions) && exceptions.Any())
+ if (!xamlc.Execute(exceptions) && exceptions.Any())
throw exceptions [0];
}
}
diff --git a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
index a5ebbd92..9765c7b9 100644
--- a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
+++ b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
@@ -368,12 +368,15 @@
<Compile Include="XStaticException.xaml.cs">
<DependentUpon>XStaticException.xaml</DependentUpon>
</Compile>
- <Compile Include="CompiledTypeConverter.xaml.cs">
- <DependentUpon>CompiledTypeConverter.xaml</DependentUpon>
+ <Compile Include="CompiledTypeConverter.xaml.cs">
+ <DependentUpon>CompiledTypeConverter.xaml</DependentUpon>
+ </Compile>
+ <Compile Include="Issues\Bz43301.xaml.cs">
+ <DependentUpon>Bz43301.xaml</DependentUpon>
+ </Compile>
+ <Compile Include="BindingsCompiler.xaml.cs">
+ <DependentUpon>BindingsCompiler.xaml</DependentUpon>
</Compile>
- <Compile Include="Issues\Bz43301.xaml.cs">
- <DependentUpon>Bz43301.xaml</DependentUpon>
- </Compile>
</ItemGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="..\.nuspec\Xamarin.Forms.Debug.targets" />
@@ -665,6 +668,9 @@
<EmbeddedResource Include="Issues\Bz43301.xaml">
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
</EmbeddedResource>
+ <EmbeddedResource Include="BindingsCompiler.xaml">
+ <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
+ </EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
diff --git a/Xamarin.Forms.Xaml.Xamlc/Xamlc.cs b/Xamarin.Forms.Xaml.Xamlc/Xamlc.cs
index b2fa172b..5a42fa02 100644
--- a/Xamarin.Forms.Xaml.Xamlc/Xamlc.cs
+++ b/Xamarin.Forms.Xaml.Xamlc/Xamlc.cs
@@ -19,6 +19,8 @@ namespace Xamarin.Forms.Xaml
bool decompile = false;
string paths = null;
string refs = null;
+ List<string> extra = null;
+
var p = new OptionSet
{
{ "h|?|help", "Print this help message", v => help = true },
@@ -35,7 +37,6 @@ namespace Xamarin.Forms.Xaml
ShowHelp(p);
Environment.Exit(0);
}
- List<string> extra = null;
try
{
extra = p.Parse(args);
@@ -57,7 +58,16 @@ namespace Xamarin.Forms.Xaml
}
var assembly = extra[0];
- XamlCTask.Compile(assembly, verbosity, keep, optimize, paths, refs, decompile);
+ var xamlc = new XamlCTask {
+ Assembly = assembly,
+ Verbosity = verbosity,
+ KeepXamlResources = keep,
+ OptimizeIL = optimize,
+ DependencyPaths = paths,
+ ReferencePath = refs,
+ OutputGeneratedILAsCode=decompile,
+ };
+ xamlc.Execute(null);
}
static void ShowHelp(OptionSet ops)
diff --git a/Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs b/Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs
index 36f5b7fd..154ba023 100644
--- a/Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs
+++ b/Xamarin.Forms.Xaml/ApplyPropertiesVisitor.cs
@@ -19,7 +19,8 @@ namespace Xamarin.Forms.Xaml
XmlName.xTypeArguments,
XmlName.xArguments,
XmlName.xFactoryMethod,
- XmlName.xName
+ XmlName.xName,
+ XmlName.xDataType
};
public ApplyPropertiesVisitor(HydratationContext context, bool stopOnResourceDictionary = false)
diff --git a/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs b/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs
index 36e8fc17..81893506 100644
--- a/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs
+++ b/Xamarin.Forms.Xaml/ExpandMarkupsVisitor.cs
@@ -17,7 +17,8 @@ namespace Xamarin.Forms.Xaml
XmlName.xKey,
XmlName.xTypeArguments,
XmlName.xFactoryMethod,
- XmlName.xName
+ XmlName.xName,
+ XmlName.xDataType
};
Dictionary<INode, object> Values
diff --git a/Xamarin.Forms.Xaml/MarkupExtensions/BindingExtension.cs b/Xamarin.Forms.Xaml/MarkupExtensions/BindingExtension.cs
index 5b519e62..df82771d 100644
--- a/Xamarin.Forms.Xaml/MarkupExtensions/BindingExtension.cs
+++ b/Xamarin.Forms.Xaml/MarkupExtensions/BindingExtension.cs
@@ -1,4 +1,5 @@
using System;
+using Xamarin.Forms.Internals;
namespace Xamarin.Forms.Xaml
{
@@ -25,9 +26,20 @@ namespace Xamarin.Forms.Xaml
public string UpdateSourceEventName { get; set; }
+ public TypedBindingBase TypedBinding { get; set; }
+
BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
{
- return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { UpdateSourceEventName = UpdateSourceEventName };
+ if (TypedBinding == null)
+ return new Binding(Path, Mode, Converter, ConverterParameter, StringFormat, Source) { UpdateSourceEventName = UpdateSourceEventName };
+
+ TypedBinding.Mode = Mode;
+ TypedBinding.Converter = Converter;
+ TypedBinding.ConverterParameter = ConverterParameter;
+ TypedBinding.StringFormat = StringFormat;
+ TypedBinding.Source = Source;
+ TypedBinding.UpdateSourceEventName = UpdateSourceEventName;
+ return TypedBinding;
}
object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
diff --git a/Xamarin.Forms.Xaml/XamlCompilationAttribute.cs b/Xamarin.Forms.Xaml/XamlCompilationAttribute.cs
index 76199039..79cc87bd 100644
--- a/Xamarin.Forms.Xaml/XamlCompilationAttribute.cs
+++ b/Xamarin.Forms.Xaml/XamlCompilationAttribute.cs
@@ -21,7 +21,7 @@ namespace Xamarin.Forms.Xaml
public XamlCompilationOptions XamlCompilationOptions { get; set; }
}
- internal static class XamlCExtensions
+ static class XamlCExtensions
{
public static bool IsCompiled(this Type type)
{
diff --git a/Xamarin.Forms.Xaml/XamlNode.cs b/Xamarin.Forms.Xaml/XamlNode.cs
index a19e7d5c..752f3845 100644
--- a/Xamarin.Forms.Xaml/XamlNode.cs
+++ b/Xamarin.Forms.Xaml/XamlNode.cs
@@ -42,7 +42,7 @@ namespace Xamarin.Forms.Xaml
}
[DebuggerDisplay("{NamespaceUri}:{Name}")]
- internal class XmlType
+ class XmlType
{
public XmlType(string namespaceUri, string name, IList<XmlType> typeArguments)
{
@@ -53,8 +53,7 @@ namespace Xamarin.Forms.Xaml
public string NamespaceUri { get; }
public string Name { get; }
- public IList<XmlType> TypeArguments { get; private set; }
-
+ public IList<XmlType> TypeArguments { get; }
}
internal abstract class BaseNode : IXmlLineInfo, INode
diff --git a/Xamarin.Forms.Xaml/XamlParser.cs b/Xamarin.Forms.Xaml/XamlParser.cs
index 60424754..f2ac3197 100644
--- a/Xamarin.Forms.Xaml/XamlParser.cs
+++ b/Xamarin.Forms.Xaml/XamlParser.cs
@@ -197,47 +197,48 @@ namespace Xamarin.Forms.Xaml
if (reader.NamespaceURI == "http://schemas.microsoft.com/winfx/2006/xaml")
{
- switch (reader.Name)
- {
- case "x:Key":
- propertyName = XmlName.xKey;
- break;
- case "x:Name":
- propertyName = XmlName.xName;
- break;
- case "x:Class":
- continue;
- default:
- Debug.WriteLine("Unhandled {0}", reader.Name);
- continue;
+ switch (reader.Name) {
+ case "x:Key":
+ propertyName = XmlName.xKey;
+ break;
+ case "x:Name":
+ propertyName = XmlName.xName;
+ break;
+ case "x:Class":
+ continue;
+ default:
+ Debug.WriteLine("Unhandled attribute {0}", reader.Name);
+ continue;
}
}
if (reader.NamespaceURI == "http://schemas.microsoft.com/winfx/2009/xaml")
{
- switch (reader.Name)
- {
- case "x:Key":
- propertyName = XmlName.xKey;
- break;
- case "x:Name":
- propertyName = XmlName.xName;
- break;
- case "x:TypeArguments":
- propertyName = XmlName.xTypeArguments;
- value = TypeArgumentsParser.ParseExpression((string)value, (IXmlNamespaceResolver)reader, (IXmlLineInfo)reader);
- break;
- case "x:Class":
- continue;
- case "x:FactoryMethod":
- propertyName = XmlName.xFactoryMethod;
- break;
- case "x:Arguments":
- propertyName = XmlName.xArguments;
+ switch (reader.Name) {
+ case "x:Key":
+ propertyName = XmlName.xKey;
+ break;
+ case "x:Name":
+ propertyName = XmlName.xName;
break;
- default:
- Debug.WriteLine("Unhandled {0}", reader.Name);
- continue;
+ case "x:TypeArguments":
+ propertyName = XmlName.xTypeArguments;
+ value = TypeArgumentsParser.ParseExpression((string)value, (IXmlNamespaceResolver)reader, (IXmlLineInfo)reader);
+ break;
+ case "x:DataType":
+ propertyName = XmlName.xDataType;
+ break;
+ case "x:Class":
+ continue;
+ case "x:FactoryMethod":
+ propertyName = XmlName.xFactoryMethod;
+ break;
+ case "x:Arguments":
+ propertyName = XmlName.xArguments;
+ break;
+ default:
+ Debug.WriteLine("Unhandled attribute {0}", reader.Name);
+ continue;
}
}
diff --git a/Xamarin.Forms.Xaml/XmlName.cs b/Xamarin.Forms.Xaml/XmlName.cs
index 09cf3bca..92e1fc04 100644
--- a/Xamarin.Forms.Xaml/XmlName.cs
+++ b/Xamarin.Forms.Xaml/XmlName.cs
@@ -11,6 +11,7 @@ namespace Xamarin.Forms.Xaml
public static readonly XmlName xTypeArguments = new XmlName("x", "TypeArguments");
public static readonly XmlName xArguments = new XmlName("x", "Arguments");
public static readonly XmlName xFactoryMethod = new XmlName("x", "xFactoryMethod");
+ public static readonly XmlName xDataType = new XmlName("x", "DataType");
public static readonly XmlName Empty = new XmlName();
public string NamespaceURI { get; }
diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBindingBase.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBindingBase.xml
new file mode 100644
index 00000000..354ceb6b
--- /dev/null
+++ b/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBindingBase.xml
@@ -0,0 +1,66 @@
+<Type Name="TypedBindingBase" FullName="Xamarin.Forms.Internals.TypedBindingBase">
+ <TypeSignature Language="C#" Value="public abstract class TypedBindingBase : Xamarin.Forms.BindingBase" />
+ <TypeSignature Language="ILAsm" Value=".class public auto ansi abstract beforefieldinit TypedBindingBase extends Xamarin.Forms.BindingBase" />
+ <AssemblyInfo>
+ <AssemblyName>Xamarin.Forms.Core</AssemblyName>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <Base>
+ <BaseTypeName>Xamarin.Forms.BindingBase</BaseTypeName>
+ </Base>
+ <Interfaces />
+ <Docs>
+ <summary>To be added.</summary>
+ <remarks>To be added.</remarks>
+ </Docs>
+ <Members>
+ <Member MemberName="Converter">
+ <MemberSignature Language="C#" Value="public Xamarin.Forms.IValueConverter Converter { get; set; }" />
+ <MemberSignature Language="ILAsm" Value=".property instance class Xamarin.Forms.IValueConverter Converter" />
+ <MemberType>Property</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <ReturnValue>
+ <ReturnType>Xamarin.Forms.IValueConverter</ReturnType>
+ </ReturnValue>
+ <Docs>
+ <summary>To be added.</summary>
+ <value>To be added.</value>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
+ <Member MemberName="ConverterParameter">
+ <MemberSignature Language="C#" Value="public object ConverterParameter { get; set; }" />
+ <MemberSignature Language="ILAsm" Value=".property instance object ConverterParameter" />
+ <MemberType>Property</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <ReturnValue>
+ <ReturnType>System.Object</ReturnType>
+ </ReturnValue>
+ <Docs>
+ <summary>To be added.</summary>
+ <value>To be added.</value>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
+ <Member MemberName="Source">
+ <MemberSignature Language="C#" Value="public object Source { get; set; }" />
+ <MemberSignature Language="ILAsm" Value=".property instance object Source" />
+ <MemberType>Property</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <ReturnValue>
+ <ReturnType>System.Object</ReturnType>
+ </ReturnValue>
+ <Docs>
+ <summary>To be added.</summary>
+ <value>To be added.</value>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
+ </Members>
+</Type>
diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBinding`2.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBinding`2.xml
new file mode 100644
index 00000000..d31d0f99
--- /dev/null
+++ b/docs/Xamarin.Forms.Core/Xamarin.Forms.Internals/TypedBinding`2.xml
@@ -0,0 +1,44 @@
+<Type Name="TypedBinding&lt;TSource,TProperty&gt;" FullName="Xamarin.Forms.Internals.TypedBinding&lt;TSource,TProperty&gt;">
+ <TypeSignature Language="C#" Value="public sealed class TypedBinding&lt;TSource,TProperty&gt; : Xamarin.Forms.Internals.TypedBindingBase" />
+ <TypeSignature Language="ILAsm" Value=".class public auto ansi sealed beforefieldinit TypedBinding`2&lt;TSource, TProperty&gt; extends Xamarin.Forms.Internals.TypedBindingBase" />
+ <AssemblyInfo>
+ <AssemblyName>Xamarin.Forms.Core</AssemblyName>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <TypeParameters>
+ <TypeParameter Name="TSource" />
+ <TypeParameter Name="TProperty" />
+ </TypeParameters>
+ <Base>
+ <BaseTypeName>Xamarin.Forms.Internals.TypedBindingBase</BaseTypeName>
+ </Base>
+ <Interfaces />
+ <Docs>
+ <typeparam name="TSource">To be added.</typeparam>
+ <typeparam name="TProperty">To be added.</typeparam>
+ <summary>To be added.</summary>
+ <remarks>To be added.</remarks>
+ </Docs>
+ <Members>
+ <Member MemberName=".ctor">
+ <MemberSignature Language="C#" Value="public TypedBinding (Func&lt;TSource,TProperty&gt; getter, Action&lt;TSource,TProperty&gt; setter, Tuple&lt;Func&lt;TSource,object&gt;,string&gt;[] handlers);" />
+ <MemberSignature Language="ILAsm" Value=".method public hidebysig specialname rtspecialname instance void .ctor(class System.Func`2&lt;!TSource, !TProperty&gt; getter, class System.Action`2&lt;!TSource, !TProperty&gt; setter, class System.Tuple`2&lt;class System.Func`2&lt;!TSource, object&gt;, string&gt;[] handlers) cil managed" />
+ <MemberType>Constructor</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <Parameters>
+ <Parameter Name="getter" Type="System.Func&lt;TSource,TProperty&gt;" />
+ <Parameter Name="setter" Type="System.Action&lt;TSource,TProperty&gt;" />
+ <Parameter Name="handlers" Type="System.Tuple&lt;System.Func&lt;TSource,System.Object&gt;,System.String&gt;[]" />
+ </Parameters>
+ <Docs>
+ <param name="getter">To be added.</param>
+ <param name="setter">To be added.</param>
+ <param name="handlers">To be added.</param>
+ <summary>To be added.</summary>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
+ </Members>
+</Type>
diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObject.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObject.xml
index ebfa93f9..e1310cac 100644
--- a/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObject.xml
+++ b/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObject.xml
@@ -135,6 +135,22 @@ public static void OneWayDemo ()
</Docs>
</Member>
<Member MemberName="ApplyBindings">
+ <MemberSignature Language="C#" Value="protected void ApplyBindings ();" />
+ <MemberSignature Language="ILAsm" Value=".method familyhidebysig instance void ApplyBindings() cil managed" />
+ <MemberType>Method</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <ReturnValue>
+ <ReturnType>System.Void</ReturnType>
+ </ReturnValue>
+ <Parameters />
+ <Docs>
+ <summary>To be added.</summary>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
+ <Member MemberName="ApplyBindings">
<MemberSignature Language="C#" Value="protected void ApplyBindings (object oldContext = null);" />
<MemberSignature Language="ILAsm" Value=".method familyhidebysig instance void ApplyBindings(object oldContext) cil managed" />
<MemberType>Method</MemberType>
diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObjectExtensions.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObjectExtensions.xml
index a37ea69f..a99bf66f 100644
--- a/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObjectExtensions.xml
+++ b/docs/Xamarin.Forms.Core/Xamarin.Forms/BindableObjectExtensions.xml
@@ -111,6 +111,11 @@ Debug.WriteLine (label.Text); //prints "John Doe"
<AssemblyVersion>1.5.0.0</AssemblyVersion>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
+ <Attributes>
+ <Attribute>
+ <AttributeName>System.Obsolete</AttributeName>
+ </Attribute>
+ </Attributes>
<ReturnValue>
<ReturnType>System.Void</ReturnType>
</ReturnValue>
diff --git a/docs/Xamarin.Forms.Core/Xamarin.Forms/Binding.xml b/docs/Xamarin.Forms.Core/Xamarin.Forms/Binding.xml
index f7e24286..0235f316 100644
--- a/docs/Xamarin.Forms.Core/Xamarin.Forms/Binding.xml
+++ b/docs/Xamarin.Forms.Core/Xamarin.Forms/Binding.xml
@@ -232,6 +232,11 @@ Debug.WriteLine (person.Name); //prints "Foo". ReverseConverter.ConvertBack () i
<AssemblyVersion>1.5.0.0</AssemblyVersion>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
</AssemblyInfo>
+ <Attributes>
+ <Attribute>
+ <AttributeName>System.Obsolete</AttributeName>
+ </Attribute>
+ </Attributes>
<ReturnValue>
<ReturnType>Xamarin.Forms.Binding</ReturnType>
</ReturnValue>
diff --git a/docs/Xamarin.Forms.Core/index.xml b/docs/Xamarin.Forms.Core/index.xml
index b4b3e8fd..ec79af75 100644
--- a/docs/Xamarin.Forms.Core/index.xml
+++ b/docs/Xamarin.Forms.Core/index.xml
@@ -454,6 +454,8 @@
<Type Name="PreserveAttribute" Kind="Class" />
<Type Name="Ticker" Kind="Class" />
<Type Name="ToolbarTracker" Kind="Class" />
+ <Type Name="TypedBinding`2" DisplayName="TypedBinding&lt;TSource,TProperty&gt;" Kind="Class" />
+ <Type Name="TypedBindingBase" Kind="Class" />
</Namespace>
<Namespace Name="Xamarin.Forms.PlatformConfiguration">
<Type Name="Android" Kind="Class" />
diff --git a/docs/Xamarin.Forms.Xaml/Xamarin.Forms.Xaml/BindingExtension.xml b/docs/Xamarin.Forms.Xaml/Xamarin.Forms.Xaml/BindingExtension.xml
index f040bb89..6bad86cb 100644
--- a/docs/Xamarin.Forms.Xaml/Xamarin.Forms.Xaml/BindingExtension.xml
+++ b/docs/Xamarin.Forms.Xaml/Xamarin.Forms.Xaml/BindingExtension.xml
@@ -148,6 +148,22 @@
<remarks>To be added.</remarks>
</Docs>
</Member>
+ <Member MemberName="TypedBinding">
+ <MemberSignature Language="C#" Value="public Xamarin.Forms.Internals.TypedBindingBase TypedBinding { get; set; }" />
+ <MemberSignature Language="ILAsm" Value=".property instance class Xamarin.Forms.Internals.TypedBindingBase TypedBinding" />
+ <MemberType>Property</MemberType>
+ <AssemblyInfo>
+ <AssemblyVersion>2.0.0.0</AssemblyVersion>
+ </AssemblyInfo>
+ <ReturnValue>
+ <ReturnType>Xamarin.Forms.Internals.TypedBindingBase</ReturnType>
+ </ReturnValue>
+ <Docs>
+ <summary>To be added.</summary>
+ <value>To be added.</value>
+ <remarks>To be added.</remarks>
+ </Docs>
+ </Member>
<Member MemberName="UpdateSourceEventName">
<MemberSignature Language="C#" Value="public string UpdateSourceEventName { get; set; }" />
<MemberSignature Language="ILAsm" Value=".property instance string UpdateSourceEventName" />