BindingExpression.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.Linq.Expressions;
  7. using System.Text;
  8. using Avalonia.Data.Converters;
  9. using Avalonia.Data.Core.ExpressionNodes;
  10. using Avalonia.Data.Core.Parsers;
  11. using Avalonia.Input;
  12. using Avalonia.Interactivity;
  13. using Avalonia.Logging;
  14. using Avalonia.Threading;
  15. using Avalonia.Utilities;
  16. namespace Avalonia.Data.Core;
  17. /// <summary>
  18. /// A binding expression which accepts and produces (possibly boxed) object values.
  19. /// </summary>
  20. /// <remarks>
  21. /// A <see cref="BindingExpression"/> represents a untyped binding which has been
  22. /// instantiated on an object.
  23. /// </remarks>
  24. internal partial class BindingExpression : UntypedBindingExpressionBase, IDescription, IDisposable
  25. {
  26. private static readonly List<ExpressionNode> s_emptyExpressionNodes = new();
  27. private readonly WeakReference<object?>? _source;
  28. private readonly BindingMode _mode;
  29. private readonly List<ExpressionNode> _nodes;
  30. private readonly TargetTypeConverter? _targetTypeConverter;
  31. private readonly UncommonFields? _uncommon;
  32. private bool _shouldUpdateOneTimeBindingTarget;
  33. /// <summary>
  34. /// Initializes a new instance of the <see cref="BindingExpression"/> class.
  35. /// </summary>
  36. /// <param name="source">The source from which the value will be read.</param>
  37. /// <param name="nodes">The nodes representing the binding path.</param>
  38. /// <param name="fallbackValue">
  39. /// The fallback value. Pass <see cref="AvaloniaProperty.UnsetValue"/> for no fallback.
  40. /// </param>
  41. /// <param name="delay">The amount of time to wait before updating the binding source after the value on the target changes.</param>
  42. /// <param name="converter">The converter to use.</param>
  43. /// <param name="converterCulture">The converter culture to use.</param>
  44. /// <param name="converterParameter">The converter parameter.</param>
  45. /// <param name="enableDataValidation">
  46. /// Whether data validation should be enabled for the binding.
  47. /// </param>
  48. /// <param name="mode">The binding mode.</param>
  49. /// <param name="priority">The binding priority.</param>
  50. /// <param name="stringFormat">The format string to use.</param>
  51. /// <param name="targetProperty">The target property being bound to.</param>
  52. /// <param name="targetNullValue">The null target value.</param>
  53. /// <param name="targetTypeConverter">
  54. /// A final type converter to be run on the produced value.
  55. /// </param>
  56. /// <param name="updateSourceTrigger">The trigger for updating the source value.</param>
  57. public BindingExpression(
  58. object? source,
  59. List<ExpressionNode>? nodes,
  60. object? fallbackValue,
  61. TimeSpan delay = default,
  62. IValueConverter? converter = null,
  63. CultureInfo? converterCulture = null,
  64. object? converterParameter = null,
  65. bool enableDataValidation = false,
  66. BindingMode mode = BindingMode.OneWay,
  67. BindingPriority priority = BindingPriority.LocalValue,
  68. string? stringFormat = null,
  69. object? targetNullValue = null,
  70. AvaloniaProperty? targetProperty = null,
  71. TargetTypeConverter? targetTypeConverter = null,
  72. UpdateSourceTrigger updateSourceTrigger = UpdateSourceTrigger.PropertyChanged)
  73. : base(priority, targetProperty, enableDataValidation)
  74. {
  75. if (mode == BindingMode.Default)
  76. throw new ArgumentException("Binding mode cannot be Default.", nameof(mode));
  77. if (updateSourceTrigger == UpdateSourceTrigger.Default)
  78. throw new ArgumentException("UpdateSourceTrigger cannot be Default.", nameof(updateSourceTrigger));
  79. if (source == AvaloniaProperty.UnsetValue)
  80. source = null;
  81. _source = new(source);
  82. _mode = mode;
  83. _nodes = nodes ?? s_emptyExpressionNodes;
  84. _targetTypeConverter = targetTypeConverter;
  85. _shouldUpdateOneTimeBindingTarget = _mode == BindingMode.OneTime;
  86. if (delay != default ||
  87. converter is not null ||
  88. converterCulture is not null ||
  89. converterParameter is not null ||
  90. fallbackValue != AvaloniaProperty.UnsetValue ||
  91. !string.IsNullOrWhiteSpace(stringFormat) ||
  92. (targetNullValue is not null && targetNullValue != AvaloniaProperty.UnsetValue) ||
  93. updateSourceTrigger is not UpdateSourceTrigger.PropertyChanged)
  94. {
  95. _uncommon = new()
  96. {
  97. _delay = delay,
  98. _converter = converter,
  99. _converterCulture = converterCulture,
  100. _converterParameter = converterParameter,
  101. _fallbackValue = fallbackValue,
  102. _stringFormat = stringFormat switch
  103. {
  104. string s when string.IsNullOrWhiteSpace(s) => null,
  105. string s when !s.Contains('{') => $"{{0:{stringFormat}}}",
  106. _ => stringFormat,
  107. },
  108. _targetNullValue = targetNullValue ?? AvaloniaProperty.UnsetValue,
  109. _updateSourceTrigger = updateSourceTrigger,
  110. };
  111. }
  112. IPropertyAccessorNode? leafAccessor = null;
  113. if (nodes is not null)
  114. {
  115. for (var i = 0; i < nodes.Count; ++i)
  116. {
  117. var node = nodes[i];
  118. node.SetOwner(this, i);
  119. if (node is IPropertyAccessorNode n)
  120. leafAccessor = n;
  121. }
  122. }
  123. if (enableDataValidation)
  124. leafAccessor?.EnableDataValidation();
  125. }
  126. public override string Description
  127. {
  128. get
  129. {
  130. var b = new StringBuilder();
  131. LeafNode.BuildString(b, _nodes);
  132. return b.ToString();
  133. }
  134. }
  135. public Type? SourceType => (LeafNode as ISettableNode)?.ValueType;
  136. public TimeSpan Delay => _uncommon?._delay ?? default;
  137. public IValueConverter? Converter => _uncommon?._converter;
  138. public CultureInfo ConverterCulture => _uncommon?._converterCulture ?? CultureInfo.CurrentCulture;
  139. public object? ConverterParameter => _uncommon?._converterParameter;
  140. public object? FallbackValue => _uncommon is not null ? _uncommon._fallbackValue : AvaloniaProperty.UnsetValue;
  141. public ExpressionNode LeafNode => _nodes[_nodes.Count - 1];
  142. public string? StringFormat => _uncommon?._stringFormat;
  143. public object? TargetNullValue => _uncommon?._targetNullValue ?? AvaloniaProperty.UnsetValue;
  144. public UpdateSourceTrigger UpdateSourceTrigger => _uncommon?._updateSourceTrigger ?? UpdateSourceTrigger.PropertyChanged;
  145. public override void UpdateSource()
  146. {
  147. if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource)
  148. WriteTargetValueToSource();
  149. }
  150. public override void UpdateTarget()
  151. {
  152. if (_nodes.Count == 0)
  153. return;
  154. var source = _nodes[0].Source;
  155. for (var i = 0; i < _nodes.Count; ++i)
  156. _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null);
  157. _nodes[0].SetSource(source, null);
  158. }
  159. /// <summary>
  160. /// Creates an <see cref="BindingExpression"/> from an expression tree.
  161. /// </summary>
  162. /// <typeparam name="TIn">The input type of the binding expression.</typeparam>
  163. /// <typeparam name="TOut">The output type of the binding expression.</typeparam>
  164. /// <param name="source">The source from which the binding value will be read.</param>
  165. /// <param name="expression">The expression representing the binding path.</param>
  166. /// <param name="converter">The converter to use.</param>
  167. /// <param name="converterCulture">The converter culture to use.</param>
  168. /// <param name="converterParameter">The converter parameter.</param>
  169. /// <param name="enableDataValidation">Whether data validation should be enabled for the binding.</param>
  170. /// <param name="fallbackValue">The fallback value.</param>
  171. /// <param name="mode">The binding mode.</param>
  172. /// <param name="priority">The binding priority.</param>
  173. /// <param name="targetNullValue">The null target value.</param>
  174. /// <param name="allowReflection">Whether to allow reflection for target type conversion.</param>
  175. [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)]
  176. internal static BindingExpression Create<TIn, TOut>(
  177. TIn source,
  178. Expression<Func<TIn, TOut>> expression,
  179. IValueConverter? converter = null,
  180. CultureInfo? converterCulture = null,
  181. object? converterParameter = null,
  182. bool enableDataValidation = false,
  183. Optional<object?> fallbackValue = default,
  184. BindingMode mode = BindingMode.OneWay,
  185. BindingPriority priority = BindingPriority.LocalValue,
  186. object? targetNullValue = null,
  187. bool allowReflection = true)
  188. where TIn : class?
  189. {
  190. var nodes = BindingExpressionVisitor<TIn>.BuildNodes(expression, enableDataValidation);
  191. var fallback = fallbackValue.HasValue ? fallbackValue.Value : AvaloniaProperty.UnsetValue;
  192. return new BindingExpression(
  193. source,
  194. nodes,
  195. fallback,
  196. converter: converter,
  197. converterCulture: converterCulture,
  198. converterParameter: converterParameter,
  199. enableDataValidation: enableDataValidation,
  200. mode: mode,
  201. priority: priority,
  202. targetNullValue: targetNullValue,
  203. targetTypeConverter: allowReflection ?
  204. TargetTypeConverter.GetReflectionConverter() :
  205. TargetTypeConverter.GetDefaultConverter());
  206. }
  207. /// <summary>
  208. /// Called by an <see cref="ExpressionNode"/> belonging to this binding when its
  209. /// <see cref="ExpressionNode.Value"/> changes.
  210. /// </summary>
  211. /// <param name="nodeIndex">The <see cref="ExpressionNode.Index"/>.</param>
  212. /// <param name="value">The <see cref="ExpressionNode.Value"/>.</param>
  213. /// <param name="dataValidationError">
  214. /// The data validation error associated with the current value, if any.
  215. /// </param>
  216. internal void OnNodeValueChanged(int nodeIndex, object? value, Exception? dataValidationError)
  217. {
  218. Debug.Assert(value is not BindingNotification);
  219. Debug.Assert(nodeIndex >= 0 && nodeIndex < _nodes.Count);
  220. if (nodeIndex == _nodes.Count - 1)
  221. {
  222. if (_mode == BindingMode.OneTime)
  223. {
  224. // In OneTime mode, only changing the data context updates the binding.
  225. if (!_shouldUpdateOneTimeBindingTarget && _nodes[nodeIndex] is not DataContextNodeBase)
  226. return;
  227. _shouldUpdateOneTimeBindingTarget = false;
  228. }
  229. // The leaf node has changed. If the binding mode is not OneWayToSource, publish the
  230. // value to the target.
  231. if (_mode != BindingMode.OneWayToSource)
  232. {
  233. var error = dataValidationError is not null ?
  234. new BindingError(dataValidationError, BindingErrorType.DataValidationError) :
  235. null;
  236. ConvertAndPublishValue(value, error);
  237. }
  238. }
  239. else if (_mode == BindingMode.OneWayToSource && nodeIndex == _nodes.Count - 2 && value is not null)
  240. {
  241. // When the binding mode is OneWayToSource, we need to write the value to the source
  242. // when the object holding the source property changes; this is node before the leaf
  243. // node. First update the leaf node's source, then write the value to its property.
  244. _nodes[nodeIndex + 1].SetSource(value, dataValidationError);
  245. WriteTargetValueToSource();
  246. }
  247. else
  248. {
  249. if (_mode == BindingMode.OneTime && _nodes[nodeIndex] is DataContextNodeBase)
  250. _shouldUpdateOneTimeBindingTarget = true;
  251. _nodes[nodeIndex + 1].SetSource(value, dataValidationError);
  252. }
  253. }
  254. /// <summary>
  255. /// Called by an <see cref="ExpressionNode"/> belonging to this binding when an error occurs
  256. /// reading its value.
  257. /// </summary>
  258. /// <param name="nodeIndex">
  259. /// The <see cref="ExpressionNode.Index"/> or -1 if the source is null.
  260. /// </param>
  261. /// <param name="error">The error message.</param>
  262. internal void OnNodeError(int nodeIndex, string error)
  263. {
  264. // Set the source of all nodes after the one that errored to unset. This needs to be done
  265. // for each node individually because setting the source to unset will not result in
  266. // OnNodeValueChanged or OnNodeError being called.
  267. for (var i = nodeIndex + 1; i < _nodes.Count; ++i)
  268. _nodes[i].SetSource(AvaloniaProperty.UnsetValue, null);
  269. if (_mode == BindingMode.OneWayToSource)
  270. return;
  271. var errorPoint = CalculateErrorPoint(nodeIndex);
  272. if (ShouldLogError(out var target))
  273. Log(target, error, errorPoint);
  274. // Clear the current value and publish the error.
  275. var bindingError = new BindingError(
  276. new BindingChainException(error, Description, errorPoint.ToString()),
  277. BindingErrorType.Error);
  278. ConvertAndPublishValue(AvaloniaProperty.UnsetValue, bindingError);
  279. }
  280. internal void OnDataValidationError(Exception error)
  281. {
  282. var bindingError = new BindingError(error, BindingErrorType.DataValidationError);
  283. PublishValue(UnchangedValue, bindingError);
  284. }
  285. internal override bool WriteValueToSource(object? value)
  286. {
  287. StopDelayTimer();
  288. if (_nodes.Count == 0 || LeafNode is not ISettableNode setter || setter.ValueType is not { } type)
  289. return false;
  290. if (Converter is { } converter &&
  291. value != AvaloniaProperty.UnsetValue &&
  292. value != BindingOperations.DoNothing)
  293. {
  294. value = ConvertBack(converter, ConverterCulture, ConverterParameter, value, type);
  295. }
  296. if (value == BindingOperations.DoNothing)
  297. return true;
  298. // Use the target type converter to convert the value to the target type if necessary.
  299. if (_targetTypeConverter is not null)
  300. {
  301. if (_targetTypeConverter.TryConvert(value, type, ConverterCulture, out var converted))
  302. {
  303. value = converted;
  304. }
  305. else if (FallbackValue != AvaloniaProperty.UnsetValue)
  306. {
  307. value = FallbackValue;
  308. }
  309. else if (IsDataValidationEnabled)
  310. {
  311. var valueString = value?.ToString() ?? "(null)";
  312. var valueTypeName = value?.GetType().FullName ?? "null";
  313. var ex = new InvalidCastException(
  314. $"Could not convert '{valueString}' ({valueTypeName}) to {type}.");
  315. OnDataValidationError(ex);
  316. return false;
  317. }
  318. else
  319. {
  320. return false;
  321. }
  322. }
  323. // Don't set the value if it's unchanged. If there is a binding error, we still have to set the value
  324. // in order to clear the error.
  325. if (TypeUtilities.IdentityEquals(LeafNode.Value, value, type) && ErrorType == BindingErrorType.None)
  326. return true;
  327. try
  328. {
  329. return setter.WriteValueToSource(value, _nodes);
  330. }
  331. catch
  332. {
  333. return false;
  334. }
  335. }
  336. protected override bool ShouldLogError([NotNullWhen(true)] out AvaloniaObject? target)
  337. {
  338. if (!TryGetTarget(out target))
  339. return false;
  340. if (_nodes.Count > 0 && _nodes[0] is SourceNode sourceNode)
  341. return sourceNode.ShouldLogErrors(target);
  342. return true;
  343. }
  344. protected override void StartCore()
  345. {
  346. if (_source?.TryGetTarget(out var source) == true)
  347. {
  348. if (_nodes.Count > 0)
  349. _nodes[0].SetSource(source, null);
  350. else
  351. ConvertAndPublishValue(source, null);
  352. if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
  353. TryGetTarget(out var target) &&
  354. TargetProperty is not null)
  355. {
  356. var trigger = UpdateSourceTrigger;
  357. if (trigger is UpdateSourceTrigger.PropertyChanged)
  358. target.PropertyChanged += OnTargetPropertyChanged;
  359. else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
  360. ie.LostFocus += OnTargetLostFocus;
  361. }
  362. }
  363. else
  364. {
  365. OnNodeError(-1, "Binding Source is null.");
  366. }
  367. }
  368. protected override void StopCore()
  369. {
  370. StopDelayTimer();
  371. foreach (var node in _nodes)
  372. node.SetSource(AvaloniaProperty.UnsetValue, null);
  373. if (_mode is BindingMode.TwoWay or BindingMode.OneWayToSource &&
  374. TryGetTarget(out var target))
  375. {
  376. var trigger = UpdateSourceTrigger;
  377. if (trigger is UpdateSourceTrigger.PropertyChanged)
  378. target.PropertyChanged -= OnTargetPropertyChanged;
  379. else if (trigger is UpdateSourceTrigger.LostFocus && target is IInputElement ie)
  380. ie.LostFocus -= OnTargetLostFocus;
  381. }
  382. }
  383. private string CalculateErrorPoint(int nodeIndex)
  384. {
  385. // Build a string describing the binding chain up to the node that errored.
  386. var result = new StringBuilder();
  387. if (nodeIndex >= 0)
  388. _nodes[nodeIndex].BuildString(result);
  389. else
  390. result.Append("(source)");
  391. return result.ToString();
  392. }
  393. private void Log(AvaloniaObject target, string error, string errorPoint, LogEventLevel level = LogEventLevel.Warning)
  394. {
  395. if (!Logger.TryGet(level, LogArea.Binding, out var log))
  396. return;
  397. log.Log(
  398. target,
  399. "An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}",
  400. (object?)TargetProperty ?? "(unknown)",
  401. Description,
  402. errorPoint,
  403. error);
  404. }
  405. private void ConvertAndPublishValue(object? value, BindingError? error)
  406. {
  407. var isTargetNullValue = false;
  408. // All values other than UnsetValue and DoNothing should be passed to the converter.
  409. if (Converter is { } converter &&
  410. value != AvaloniaProperty.UnsetValue &&
  411. value != BindingOperations.DoNothing)
  412. {
  413. value = Convert(converter, ConverterCulture, ConverterParameter, value, TargetType, ref error);
  414. }
  415. // Check this here as the converter may return DoNothing.
  416. if (value == BindingOperations.DoNothing)
  417. return;
  418. // TargetNullValue only applies when the value is null: UnsetValue indicates that there
  419. // was a binding error so we don't want to use TargetNullValue in that case.
  420. if (value is null && TargetNullValue != AvaloniaProperty.UnsetValue)
  421. {
  422. value = ConvertFallback(TargetNullValue, nameof(TargetNullValue));
  423. isTargetNullValue = true;
  424. }
  425. // If we have a value, try to convert it to the target type.
  426. if (value != AvaloniaProperty.UnsetValue)
  427. {
  428. if (StringFormat is { } stringFormat &&
  429. (TargetType == typeof(object) || TargetType == typeof(string)) &&
  430. !isTargetNullValue)
  431. {
  432. // The string format applies if we're targeting a type that can accept a string
  433. // and the value isn't the TargetNullValue.
  434. value = string.Format(ConverterCulture, stringFormat, value);
  435. }
  436. else if (_targetTypeConverter is not null)
  437. {
  438. // Otherwise, if we have a target type converter, convert the value to the target type.
  439. value = ConvertFrom(_targetTypeConverter, value, ref error);
  440. }
  441. }
  442. // FallbackValue applies if the result from the binding, converter or target type converter
  443. // is UnsetValue.
  444. if (value == AvaloniaProperty.UnsetValue && FallbackValue != AvaloniaProperty.UnsetValue)
  445. value = ConvertFallback(FallbackValue, nameof(FallbackValue));
  446. // Publish the value.
  447. PublishValue(value, error);
  448. }
  449. private void WriteTargetValueToSource()
  450. {
  451. Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
  452. StopDelayTimer();
  453. if (TryGetTarget(out var target) &&
  454. TargetProperty is not null &&
  455. target.GetValue(TargetProperty) is var value &&
  456. !TypeUtilities.IdentityEquals(value, LeafNode.Value, TargetType))
  457. {
  458. WriteValueToSource(value);
  459. }
  460. }
  461. private void OnTargetLostFocus(object? sender, RoutedEventArgs e)
  462. {
  463. Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.LostFocus);
  464. WriteTargetValueToSource();
  465. }
  466. private void OnTargetPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
  467. {
  468. Debug.Assert(_mode is BindingMode.TwoWay or BindingMode.OneWayToSource);
  469. Debug.Assert(UpdateSourceTrigger is UpdateSourceTrigger.PropertyChanged);
  470. if (e.Property != TargetProperty)
  471. return;
  472. if (_uncommon?._delay is not { Ticks: > 0 } delay)
  473. {
  474. // The value must be read from the target object instead of using the value from the event
  475. // because the value may have changed again between the time the event was raised and now.
  476. WriteTargetValueToSource();
  477. return;
  478. }
  479. if (_uncommon!._delayTimer is { } delayTimer)
  480. delayTimer.Stop();
  481. else
  482. delayTimer = _uncommon._delayTimer = new DispatcherTimer(delay, DispatcherPriority.Normal, OnDelayTimerTick) { Tag = this };
  483. delayTimer.Start();
  484. }
  485. // This is a static method so that the same delegate object can be reused by all expression instances
  486. private static void OnDelayTimerTick(object? sender, EventArgs e)
  487. {
  488. var expression = (BindingExpression)((DispatcherTimer)sender!).Tag!;
  489. expression.WriteTargetValueToSource();
  490. }
  491. private void StopDelayTimer() => _uncommon?._delayTimer?.Stop();
  492. private object? ConvertFallback(object? fallback, string fallbackName)
  493. {
  494. if (_targetTypeConverter is null || TargetType == typeof(object) || fallback == AvaloniaProperty.UnsetValue)
  495. return fallback;
  496. if (_targetTypeConverter.TryConvert(fallback, TargetType, ConverterCulture, out var result))
  497. return result;
  498. if (TryGetTarget(out var target))
  499. Log(target, $"Could not convert {fallbackName} '{fallback}' to '{TargetType}'.", LogEventLevel.Error);
  500. return AvaloniaProperty.UnsetValue;
  501. }
  502. private object? ConvertFrom(TargetTypeConverter? converter, object? value, ref BindingError? error)
  503. {
  504. if (converter is null)
  505. return value;
  506. if (converter.TryConvert(value, TargetType, ConverterCulture, out var result))
  507. return result;
  508. var valueString = value?.ToString() ?? "(null)";
  509. var valueTypeName = value?.GetType().FullName ?? "null";
  510. var message = $"Could not convert '{valueString}' ({valueTypeName}) to '{TargetType}'.";
  511. if (ShouldLogError(out var target))
  512. Log(target, message, LogEventLevel.Warning);
  513. error = new(new InvalidCastException(message), BindingErrorType.Error);
  514. return AvaloniaProperty.UnsetValue;
  515. }
  516. /// <summary>
  517. /// Uncommonly used fields are separated out to reduce memory usage.
  518. /// </summary>
  519. private class UncommonFields
  520. {
  521. public TimeSpan _delay;
  522. public DispatcherTimer? _delayTimer;
  523. public IValueConverter? _converter;
  524. public object? _converterParameter;
  525. public CultureInfo? _converterCulture;
  526. public object? _fallbackValue;
  527. public string? _stringFormat;
  528. public object? _targetNullValue;
  529. public UpdateSourceTrigger _updateSourceTrigger;
  530. }
  531. }