651 строка
19 KiB
C#
651 строка
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using Xamarin.Forms.Internals;
|
|
using System.Runtime.CompilerServices;
|
|
|
|
namespace Xamarin.Forms
|
|
{
|
|
internal class BindingExpression
|
|
{
|
|
internal const string PropertyNotFoundErrorMessage = "'{0}' property not found on '{1}', target property: '{2}.{3}'";
|
|
|
|
readonly List<BindingExpressionPart> _parts = new List<BindingExpressionPart>();
|
|
|
|
BindableProperty _targetProperty;
|
|
WeakReference<object> _weakSource;
|
|
WeakReference<BindableObject> _weakTarget;
|
|
|
|
internal BindingExpression(BindingBase binding, string path)
|
|
{
|
|
if (binding == null)
|
|
throw new ArgumentNullException(nameof(binding));
|
|
if (path == null)
|
|
throw new ArgumentNullException(nameof(path));
|
|
|
|
Binding = binding;
|
|
Path = path;
|
|
|
|
ParsePath();
|
|
}
|
|
|
|
internal BindingBase Binding { get; }
|
|
|
|
internal string Path { get; }
|
|
|
|
/// <summary>
|
|
/// Applies the binding expression to a previously set source and target.
|
|
/// </summary>
|
|
internal void Apply(bool fromTarget = false)
|
|
{
|
|
if (_weakSource == null || _weakTarget == null)
|
|
return;
|
|
|
|
BindableObject target;
|
|
if (!_weakTarget.TryGetTarget(out target))
|
|
{
|
|
Unapply();
|
|
return;
|
|
}
|
|
|
|
object source;
|
|
if (_weakSource.TryGetTarget(out source) && _targetProperty != null)
|
|
ApplyCore(source, target, _targetProperty, fromTarget);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the binding expression to a new source or target.
|
|
/// </summary>
|
|
internal void Apply(object sourceObject, BindableObject target, BindableProperty property)
|
|
{
|
|
_targetProperty = property;
|
|
|
|
BindableObject prevTarget;
|
|
if (_weakTarget != null && _weakTarget.TryGetTarget(out prevTarget) && !ReferenceEquals(prevTarget, target))
|
|
throw new InvalidOperationException("Binding instances can not be reused");
|
|
|
|
object previousSource;
|
|
if (_weakSource != null && _weakSource.TryGetTarget(out previousSource) && !ReferenceEquals(previousSource, sourceObject))
|
|
throw new InvalidOperationException("Binding instances can not be reused");
|
|
|
|
_weakSource = new WeakReference<object>(sourceObject);
|
|
_weakTarget = new WeakReference<BindableObject>(target);
|
|
|
|
ApplyCore(sourceObject, target, property);
|
|
}
|
|
|
|
internal void Unapply()
|
|
{
|
|
object sourceObject;
|
|
if (_weakSource != null && _weakSource.TryGetTarget(out sourceObject))
|
|
{
|
|
for (var i = 0; i < _parts.Count - 1; i++)
|
|
{
|
|
BindingExpressionPart part = _parts[i];
|
|
|
|
if (!part.IsSelf)
|
|
{
|
|
part.TryGetValue(sourceObject, out sourceObject);
|
|
}
|
|
|
|
part.Unsubscribe();
|
|
}
|
|
}
|
|
|
|
_weakSource = null;
|
|
_weakTarget = null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the binding expression to a previously set source or target.
|
|
/// </summary>
|
|
void ApplyCore(object sourceObject, BindableObject target, BindableProperty property, bool fromTarget = false)
|
|
{
|
|
BindingMode mode = Binding.GetRealizedMode(_targetProperty);
|
|
if ((mode == BindingMode.OneWay || mode == BindingMode.OneTime) && fromTarget)
|
|
return;
|
|
|
|
bool needsGetter = (mode == BindingMode.TwoWay && !fromTarget) || mode == BindingMode.OneWay || mode == BindingMode.OneTime;
|
|
bool needsSetter = !needsGetter && ((mode == BindingMode.TwoWay && fromTarget) || mode == BindingMode.OneWayToSource);
|
|
|
|
object current = sourceObject;
|
|
object previous = null;
|
|
BindingExpressionPart part = null;
|
|
|
|
for (var i = 0; i < _parts.Count; i++)
|
|
{
|
|
part = _parts[i];
|
|
bool isLast = i + 1 == _parts.Count;
|
|
|
|
if (!part.IsSelf && current != null)
|
|
{
|
|
// Allow the object instance itself to provide its own TypeInfo
|
|
TypeInfo currentType = current is IReflectableType reflectable ? reflectable.GetTypeInfo() : current.GetType().GetTypeInfo();
|
|
if (part.LastGetter == null || !part.LastGetter.DeclaringType.GetTypeInfo().IsAssignableFrom(currentType))
|
|
SetupPart(currentType, part);
|
|
|
|
if (!isLast)
|
|
part.TryGetValue(current, out current);
|
|
}
|
|
|
|
if ( !part.IsSelf
|
|
&& current != null
|
|
&& ( (needsGetter && part.LastGetter == null)
|
|
|| (needsSetter && part.NextPart == null && part.LastSetter == null))) {
|
|
Log.Warning("Binding", PropertyNotFoundErrorMessage, part.Content, current, target.GetType(), property.PropertyName);
|
|
break;
|
|
}
|
|
|
|
if (part.NextPart != null && (mode == BindingMode.OneWay || mode == BindingMode.TwoWay)
|
|
&& current is INotifyPropertyChanged inpc)
|
|
part.Subscribe(inpc);
|
|
|
|
previous = current;
|
|
}
|
|
|
|
Debug.Assert(part != null, "There should always be at least the self part in the expression.");
|
|
|
|
if (needsGetter)
|
|
{
|
|
object value = property.DefaultValue;
|
|
if (part.TryGetValue(current, out value) || part.IsSelf) {
|
|
value = Binding.GetSourceValue(value, property.ReturnType);
|
|
}
|
|
else
|
|
value = Binding.FallbackValue ?? property.GetDefaultValue(target);
|
|
|
|
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, SetValueFlags.ClearDynamicResource, BindableObject.SetValuePrivateFlags.Default | BindableObject.SetValuePrivateFlags.Converted);
|
|
}
|
|
else if (needsSetter && part.LastSetter != null && current != null)
|
|
{
|
|
object value = Binding.GetTargetValue(target.GetValue(property), part.SetterType);
|
|
|
|
if (!TryConvert(ref value, property, part.SetterType, false))
|
|
{
|
|
Log.Warning("Binding", "{0} can not be converted to type '{1}'", value, part.SetterType);
|
|
return;
|
|
}
|
|
|
|
object[] args;
|
|
if (part.IsIndexer)
|
|
{
|
|
args = new object[part.Arguments.Length + 1];
|
|
part.Arguments.CopyTo(args, 0);
|
|
args[args.Length - 1] = value;
|
|
}
|
|
else if (part.IsBindablePropertySetter)
|
|
{
|
|
args = new[] { part.BindablePropertyField, value };
|
|
}
|
|
else
|
|
{
|
|
args = new[] { value };
|
|
}
|
|
|
|
part.LastSetter.Invoke(current, args);
|
|
}
|
|
}
|
|
|
|
IEnumerable<BindingExpressionPart> GetPart(string part)
|
|
{
|
|
part = part.Trim();
|
|
if (part == string.Empty)
|
|
throw new FormatException("Path contains an empty part");
|
|
|
|
BindingExpressionPart indexer = null;
|
|
|
|
int lbIndex = part.IndexOf('[');
|
|
if (lbIndex != -1)
|
|
{
|
|
int rbIndex = part.LastIndexOf(']');
|
|
if (rbIndex == -1)
|
|
throw new FormatException("Indexer did not contain closing bracket");
|
|
|
|
int argLength = rbIndex - lbIndex - 1;
|
|
if (argLength == 0)
|
|
throw new FormatException("Indexer did not contain arguments");
|
|
|
|
string argString = part.Substring(lbIndex + 1, argLength);
|
|
indexer = new BindingExpressionPart(this, argString, true);
|
|
|
|
part = part.Substring(0, lbIndex);
|
|
part = part.Trim();
|
|
}
|
|
|
|
if (part.Length > 0)
|
|
yield return new BindingExpressionPart(this, part);
|
|
if (indexer != null)
|
|
yield return indexer;
|
|
}
|
|
|
|
void ParsePath()
|
|
{
|
|
string p = Path.Trim();
|
|
|
|
var last = new BindingExpressionPart(this, ".");
|
|
_parts.Add(last);
|
|
|
|
if (p[0] == '.')
|
|
{
|
|
if (p.Length == 1)
|
|
return;
|
|
|
|
p = p.Substring(1);
|
|
}
|
|
|
|
string[] pathParts = p.Split('.');
|
|
for (var i = 0; i < pathParts.Length; i++)
|
|
{
|
|
foreach (BindingExpressionPart part in GetPart(pathParts[i]))
|
|
{
|
|
last.NextPart = part;
|
|
_parts.Add(part);
|
|
last = part;
|
|
}
|
|
}
|
|
}
|
|
|
|
void SetupPart(TypeInfo sourceType, BindingExpressionPart part)
|
|
{
|
|
part.Arguments = null;
|
|
part.LastGetter = null;
|
|
part.LastSetter = null;
|
|
|
|
PropertyInfo property = null;
|
|
if (part.IsIndexer)
|
|
{
|
|
if (sourceType.IsArray)
|
|
{
|
|
int index;
|
|
if (!int.TryParse(part.Content, out index))
|
|
Log.Warning("Binding", "{0} could not be parsed as an index for a {1}", part.Content, sourceType);
|
|
else
|
|
part.Arguments = new object[] { index };
|
|
|
|
part.LastGetter = sourceType.GetDeclaredMethod("Get");
|
|
part.LastSetter = sourceType.GetDeclaredMethod("Set");
|
|
part.SetterType = sourceType.GetElementType();
|
|
}
|
|
|
|
DefaultMemberAttribute defaultMember = null;
|
|
foreach (var attrib in sourceType.GetCustomAttributes(typeof(DefaultMemberAttribute), true))
|
|
{
|
|
if (attrib is DefaultMemberAttribute d)
|
|
{
|
|
defaultMember = d;
|
|
break;
|
|
}
|
|
}
|
|
|
|
string indexerName = defaultMember != null ? defaultMember.MemberName : "Item";
|
|
|
|
part.IndexerName = indexerName;
|
|
|
|
#if NETSTANDARD2_0
|
|
try {
|
|
property = sourceType.GetDeclaredProperty(indexerName);
|
|
}
|
|
catch (AmbiguousMatchException) {
|
|
// Get most derived instance of property
|
|
foreach (var p in sourceType.GetProperties()) {
|
|
if (p.Name == indexerName && (property == null || property.DeclaringType.IsAssignableFrom(property.DeclaringType)))
|
|
property = p;
|
|
}
|
|
}
|
|
#else
|
|
property = sourceType.GetDeclaredProperty(indexerName);
|
|
#endif
|
|
|
|
if (property == null) //is the indexer defined on the base class?
|
|
property = sourceType.BaseType.GetProperty(indexerName);
|
|
if (property == null) //is the indexer defined on implemented interface ?
|
|
{
|
|
foreach (var implementedInterface in sourceType.ImplementedInterfaces)
|
|
{
|
|
property = implementedInterface.GetProperty(indexerName);
|
|
if (property != null)
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (property != null)
|
|
{
|
|
ParameterInfo parameter = null;
|
|
ParameterInfo[] array = property.GetIndexParameters();
|
|
|
|
if (array.Length > 0)
|
|
{
|
|
parameter = array[0];
|
|
}
|
|
|
|
if (parameter != null)
|
|
{
|
|
try
|
|
{
|
|
object arg = Convert.ChangeType(part.Content, parameter.ParameterType, CultureInfo.InvariantCulture);
|
|
part.Arguments = new[] { arg };
|
|
}
|
|
catch (FormatException)
|
|
{
|
|
}
|
|
catch (InvalidCastException)
|
|
{
|
|
}
|
|
catch (OverflowException)
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
TypeInfo type = sourceType;
|
|
while (type != null && property == null) {
|
|
property = type.GetDeclaredProperty(part.Content);
|
|
type = type.BaseType?.GetTypeInfo();
|
|
}
|
|
}
|
|
if (property != null)
|
|
{
|
|
if (property.CanRead && property.GetMethod.IsPublic && !property.GetMethod.IsStatic)
|
|
part.LastGetter = property.GetMethod;
|
|
if (property.CanWrite && property.SetMethod.IsPublic && !property.SetMethod.IsStatic)
|
|
{
|
|
part.LastSetter = property.SetMethod;
|
|
var lastSetterParameters = part.LastSetter.GetParameters();
|
|
part.SetterType = lastSetterParameters[lastSetterParameters.Length - 1].ParameterType;
|
|
|
|
if (Binding.AllowChaining)
|
|
{
|
|
FieldInfo bindablePropertyField = sourceType.GetDeclaredField(part.Content + "Property");
|
|
if (bindablePropertyField != null && bindablePropertyField.FieldType == typeof(BindableProperty) && sourceType.ImplementedInterfaces.Contains(typeof(IElementController)))
|
|
{
|
|
MethodInfo setValueMethod = null;
|
|
#if NETSTANDARD1_0
|
|
foreach (MethodInfo m in sourceType.AsType().GetRuntimeMethods())
|
|
{
|
|
if (m.Name.EndsWith("IElementController.SetValueFromRenderer"))
|
|
{
|
|
ParameterInfo[] parameters = m.GetParameters();
|
|
if (parameters.Length == 2 && parameters[0].ParameterType == typeof(BindableProperty))
|
|
{
|
|
setValueMethod = m;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#else
|
|
setValueMethod = typeof(IElementController).GetMethod("SetValueFromRenderer", new[] { typeof(BindableProperty), typeof(object) });
|
|
#endif
|
|
if (setValueMethod != null)
|
|
{
|
|
part.LastSetter = setValueMethod;
|
|
part.IsBindablePropertySetter = true;
|
|
part.BindablePropertyField = bindablePropertyField.GetValue(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#if !NETSTANDARD1_0
|
|
TupleElementNamesAttribute tupleEltNames;
|
|
if ( property != null
|
|
&& part.NextPart != null
|
|
&& property.PropertyType.IsGenericType
|
|
&& ( property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,>)
|
|
|| property.PropertyType.GetGenericTypeDefinition() == typeof(ValueTuple<,,,,,,,>))
|
|
&& (tupleEltNames = property.GetCustomAttribute(typeof(TupleElementNamesAttribute)) as TupleElementNamesAttribute) != null)
|
|
{
|
|
//modify the nextPart to access the tuple item via the ITuple indexer
|
|
var nextPart = part.NextPart;
|
|
var name = nextPart.Content;
|
|
var index = tupleEltNames.TransformNames.IndexOf(name);
|
|
if (index >= 0)
|
|
{
|
|
nextPart.IsIndexer = true;
|
|
nextPart.Content = index.ToString();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
}
|
|
static Type[] DecimalTypes = new[] { typeof(float), typeof(decimal), typeof(double) };
|
|
|
|
internal static bool TryConvert(ref object value, BindableProperty targetProperty, Type convertTo, bool toTarget)
|
|
{
|
|
if (value == null)
|
|
return !convertTo.GetTypeInfo().IsValueType || Nullable.GetUnderlyingType(convertTo) != null;
|
|
if ((toTarget && targetProperty.TryConvert(ref value)) || (!toTarget && convertTo.IsInstanceOfType(value)))
|
|
return true;
|
|
|
|
object original = value;
|
|
try {
|
|
var stringValue = value as string ?? string.Empty;
|
|
// see: https://bugzilla.xamarin.com/show_bug.cgi?id=32871
|
|
// do not canonicalize "*.[.]"; "1." should not update bound BindableProperty
|
|
if (stringValue.EndsWith(".", StringComparison.Ordinal) && DecimalTypes.Contains(convertTo)) {
|
|
value = original;
|
|
return false;
|
|
}
|
|
|
|
// do not canonicalize "-0"; user will likely enter a period after "-0"
|
|
if (stringValue == "-0" && DecimalTypes.Contains(convertTo)) {
|
|
value = original;
|
|
return false;
|
|
}
|
|
|
|
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 BindingPair
|
|
{
|
|
public BindingPair(BindingExpressionPart part, object source, bool isLast)
|
|
{
|
|
Part = part;
|
|
Source = source;
|
|
IsLast = isLast;
|
|
}
|
|
|
|
public bool IsLast { get; set; }
|
|
|
|
public BindingExpressionPart Part { get; private set; }
|
|
|
|
public object Source { get; private set; }
|
|
}
|
|
|
|
internal class WeakPropertyChangedProxy
|
|
{
|
|
readonly WeakReference<INotifyPropertyChanged> _source = new WeakReference<INotifyPropertyChanged>(null);
|
|
readonly WeakReference<PropertyChangedEventHandler> _listener = new WeakReference<PropertyChangedEventHandler>(null);
|
|
readonly PropertyChangedEventHandler _handler;
|
|
readonly EventHandler _bchandler;
|
|
internal WeakReference<INotifyPropertyChanged> Source => _source;
|
|
|
|
public WeakPropertyChangedProxy()
|
|
{
|
|
_handler = new PropertyChangedEventHandler(OnPropertyChanged);
|
|
_bchandler = new EventHandler(OnBCChanged);
|
|
}
|
|
|
|
public WeakPropertyChangedProxy(INotifyPropertyChanged source, PropertyChangedEventHandler listener) : this()
|
|
{
|
|
SubscribeTo(source, listener);
|
|
}
|
|
|
|
public void SubscribeTo(INotifyPropertyChanged source, PropertyChangedEventHandler listener)
|
|
{
|
|
source.PropertyChanged += _handler;
|
|
var bo = source as BindableObject;
|
|
if (bo != null)
|
|
bo.BindingContextChanged += _bchandler;
|
|
_source.SetTarget(source);
|
|
_listener.SetTarget(listener);
|
|
}
|
|
|
|
public void Unsubscribe()
|
|
{
|
|
INotifyPropertyChanged source;
|
|
if (_source.TryGetTarget(out source) && source != null)
|
|
source.PropertyChanged -= _handler;
|
|
var bo = source as BindableObject;
|
|
if (bo != null)
|
|
bo.BindingContextChanged -= _bchandler;
|
|
|
|
_source.SetTarget(null);
|
|
_listener.SetTarget(null);
|
|
}
|
|
|
|
void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
if (_listener.TryGetTarget(out var handler) && handler != null)
|
|
handler(sender, e);
|
|
else
|
|
Unsubscribe();
|
|
}
|
|
|
|
void OnBCChanged(object sender, EventArgs e)
|
|
{
|
|
OnPropertyChanged(sender, new PropertyChangedEventArgs("BindingContext"));
|
|
}
|
|
}
|
|
|
|
class BindingExpressionPart
|
|
{
|
|
readonly BindingExpression _expression;
|
|
readonly PropertyChangedEventHandler _changeHandler;
|
|
WeakPropertyChangedProxy _listener;
|
|
|
|
public BindingExpressionPart(BindingExpression expression, string content, bool isIndexer = false)
|
|
{
|
|
_expression = expression;
|
|
IsSelf = content == Forms.Binding.SelfPath;
|
|
Content = content;
|
|
IsIndexer = isIndexer;
|
|
|
|
_changeHandler = PropertyChanged;
|
|
}
|
|
|
|
public void Subscribe(INotifyPropertyChanged handler)
|
|
{
|
|
INotifyPropertyChanged source;
|
|
if (_listener != null && _listener.Source.TryGetTarget(out source) && ReferenceEquals(handler, source))
|
|
// Already subscribed
|
|
return;
|
|
|
|
// Clear out the old subscription if necessary
|
|
Unsubscribe();
|
|
|
|
_listener = new WeakPropertyChangedProxy(handler, _changeHandler);
|
|
}
|
|
|
|
public void Unsubscribe()
|
|
{
|
|
var listener = _listener;
|
|
if (listener != null)
|
|
{
|
|
listener.Unsubscribe();
|
|
_listener = null;
|
|
}
|
|
}
|
|
|
|
public object[] Arguments { get; set; }
|
|
|
|
public object BindablePropertyField { get; set; }
|
|
|
|
public string Content { get; internal set; }
|
|
|
|
public string IndexerName { get; set; }
|
|
|
|
public bool IsBindablePropertySetter { get; set; }
|
|
|
|
public bool IsIndexer { get; internal set; }
|
|
|
|
public bool IsSelf { get; }
|
|
|
|
public MethodInfo LastGetter { get; set; }
|
|
|
|
public MethodInfo LastSetter { get; set; }
|
|
|
|
public BindingExpressionPart NextPart { get; set; }
|
|
|
|
public Type SetterType { get; set; }
|
|
|
|
public void PropertyChanged(object sender, PropertyChangedEventArgs args)
|
|
{
|
|
BindingExpressionPart part = NextPart ?? this;
|
|
|
|
string name = args.PropertyName;
|
|
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
if (part.IsIndexer)
|
|
{
|
|
if (name.Contains("["))
|
|
{
|
|
if (name != string.Format("{0}[{1}]", part.IndexerName, part.Content))
|
|
return;
|
|
}
|
|
else if (name != part.IndexerName)
|
|
return;
|
|
}
|
|
else if (name != part.Content)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Device.BeginInvokeOnMainThread(() => _expression.Apply());
|
|
}
|
|
|
|
public bool TryGetValue(object source, out object value)
|
|
{
|
|
value = source;
|
|
|
|
if (LastGetter != null && value != null)
|
|
{
|
|
if (IsIndexer)
|
|
{
|
|
try {
|
|
value = LastGetter.Invoke(value, Arguments);
|
|
}
|
|
catch (TargetInvocationException ex) {
|
|
if (ex.InnerException is KeyNotFoundException || ex.InnerException is IndexOutOfRangeException || ex.InnerException is ArgumentOutOfRangeException) {
|
|
value = null;
|
|
return false;
|
|
}
|
|
else
|
|
throw ex.InnerException;
|
|
}
|
|
return true;
|
|
}
|
|
value = LastGetter.Invoke(value, Arguments);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|