diff options
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<TSource,TProperty>" FullName="Xamarin.Forms.Internals.TypedBinding<TSource,TProperty>"> + <TypeSignature Language="C#" Value="public sealed class TypedBinding<TSource,TProperty> : Xamarin.Forms.Internals.TypedBindingBase" /> + <TypeSignature Language="ILAsm" Value=".class public auto ansi sealed beforefieldinit TypedBinding`2<TSource, TProperty> 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<TSource,TProperty> getter, Action<TSource,TProperty> setter, Tuple<Func<TSource,object>,string>[] handlers);" /> + <MemberSignature Language="ILAsm" Value=".method public hidebysig specialname rtspecialname instance void .ctor(class System.Func`2<!TSource, !TProperty> getter, class System.Action`2<!TSource, !TProperty> setter, class System.Tuple`2<class System.Func`2<!TSource, object>, string>[] handlers) cil managed" /> + <MemberType>Constructor</MemberType> + <AssemblyInfo> + <AssemblyVersion>2.0.0.0</AssemblyVersion> + </AssemblyInfo> + <Parameters> + <Parameter Name="getter" Type="System.Func<TSource,TProperty>" /> + <Parameter Name="setter" Type="System.Action<TSource,TProperty>" /> + <Parameter Name="handlers" Type="System.Tuple<System.Func<TSource,System.Object>,System.String>[]" /> + </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<TSource,TProperty>" 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" /> |