ChangePropertyAction.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. using System;
  2. using System.Diagnostics.CodeAnalysis;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Reflection;
  6. using Avalonia.Controls;
  7. namespace Avalonia.Xaml.Interactions.Core;
  8. /// <summary>
  9. /// An action that will change a specified property to a specified value when invoked.
  10. /// </summary>
  11. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  12. public class ChangePropertyAction : Avalonia.Xaml.Interactivity.Action
  13. {
  14. private static readonly char[] s_trimChars = ['(', ')'];
  15. private static readonly char[] s_separator = ['.'];
  16. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  17. private static Type? GetTypeByName(string name)
  18. {
  19. return
  20. AppDomain.CurrentDomain.GetAssemblies()
  21. .Reverse()
  22. .Select(assembly => assembly.GetType(name))
  23. .FirstOrDefault(t => t is not null)
  24. ??
  25. AppDomain.CurrentDomain.GetAssemblies()
  26. .Reverse()
  27. .SelectMany(assembly => assembly.GetTypes())
  28. .FirstOrDefault(t => t.Name == name);
  29. }
  30. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  31. private static AvaloniaProperty? FindAttachedProperty(object? targetObject, string propertyName)
  32. {
  33. if (targetObject is null)
  34. {
  35. return null;
  36. }
  37. var propertyNames = propertyName.Trim().Trim(s_trimChars).Split(s_separator);
  38. if (propertyNames.Length != 2)
  39. {
  40. return null;
  41. }
  42. var targetPropertyTypeName = propertyNames[0];
  43. var targetPropertyName = propertyNames[1];
  44. var targetType = GetTypeByName(targetPropertyTypeName) ?? targetObject.GetType();
  45. var registeredAttached = AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(targetType);
  46. foreach (var avaloniaProperty in registeredAttached)
  47. {
  48. if (avaloniaProperty.OwnerType.Name == targetPropertyTypeName && avaloniaProperty.Name == targetPropertyName)
  49. {
  50. return avaloniaProperty;
  51. }
  52. }
  53. var registeredInherited = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(targetType);
  54. foreach (var avaloniaProperty in registeredInherited)
  55. {
  56. if (avaloniaProperty.Name == targetPropertyName)
  57. {
  58. return avaloniaProperty;
  59. }
  60. }
  61. return null;
  62. }
  63. /// <summary>
  64. /// Identifies the <seealso cref="PropertyName"/> avalonia property.
  65. /// </summary>
  66. public static readonly StyledProperty<string?> PropertyNameProperty =
  67. AvaloniaProperty.Register<ChangePropertyAction, string?>(nameof(PropertyName));
  68. /// <summary>
  69. /// Identifies the <seealso cref="TargetObject"/> avalonia property.
  70. /// </summary>
  71. public static readonly StyledProperty<object?> TargetObjectProperty =
  72. AvaloniaProperty.Register<ChangePropertyAction, object?>(nameof(TargetObject));
  73. /// <summary>
  74. /// Identifies the <seealso cref="Value"/> avalonia property.
  75. /// </summary>
  76. public static readonly StyledProperty<object?> ValueProperty =
  77. AvaloniaProperty.Register<ChangePropertyAction, object?>(nameof(Value));
  78. /// <summary>
  79. /// Gets or sets the name of the property to change. This is a avalonia property.
  80. /// </summary>
  81. public string? PropertyName
  82. {
  83. get => GetValue(PropertyNameProperty);
  84. set => SetValue(PropertyNameProperty, value);
  85. }
  86. /// <summary>
  87. /// Gets or sets the value to set. This is a avalonia property.
  88. /// </summary>
  89. public object? Value
  90. {
  91. get => GetValue(ValueProperty);
  92. set => SetValue(ValueProperty, value);
  93. }
  94. /// <summary>
  95. /// Gets or sets the object whose property will be changed.
  96. /// If <seealso cref="TargetObject"/> is not set or cannot be resolved, the sender of <seealso cref="Execute"/> will be used. This is a avalonia property.
  97. /// </summary>
  98. [ResolveByName]
  99. public object? TargetObject
  100. {
  101. get => GetValue(TargetObjectProperty);
  102. set => SetValue(TargetObjectProperty, value);
  103. }
  104. /// <summary>
  105. /// Executes the action.
  106. /// </summary>
  107. /// <param name="sender">The <see cref="object"/> that is passed to the action by the behavior. Generally this is <seealso cref="Avalonia.Xaml.Interactivity.IBehavior.AssociatedObject"/> or a target object.</param>
  108. /// <param name="parameter">The value of this parameter is determined by the caller.</param>
  109. /// <returns>True if updating the property value succeeds; else false.</returns>
  110. public override object Execute(object? sender, object? parameter)
  111. {
  112. if (!IsEnabled)
  113. {
  114. return false;
  115. }
  116. object? targetObject;
  117. if (GetValue(TargetObjectProperty) is not null)
  118. {
  119. targetObject = TargetObject;
  120. }
  121. else
  122. {
  123. targetObject = sender;
  124. }
  125. if (targetObject is null)
  126. {
  127. return false;
  128. }
  129. var propertyName = PropertyName;
  130. if (propertyName is null)
  131. {
  132. return false;
  133. }
  134. if (targetObject is AvaloniaObject avaloniaObject)
  135. {
  136. if (propertyName.Contains('.'))
  137. {
  138. var avaloniaProperty = FindAttachedProperty(targetObject, propertyName);
  139. if (avaloniaProperty is not null)
  140. {
  141. UpdateAvaloniaPropertyValue(avaloniaObject, avaloniaProperty);
  142. return true;
  143. }
  144. return false;
  145. }
  146. else
  147. {
  148. var avaloniaProperty = AvaloniaPropertyRegistry.Instance.FindRegistered(avaloniaObject, propertyName);
  149. if (avaloniaProperty is not null)
  150. {
  151. UpdateAvaloniaPropertyValue(avaloniaObject, avaloniaProperty);
  152. return true;
  153. }
  154. }
  155. }
  156. UpdatePropertyValue(targetObject);
  157. return true;
  158. }
  159. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  160. private void UpdatePropertyValue(object targetObject)
  161. {
  162. var propertyName = PropertyName;
  163. if (propertyName is null)
  164. {
  165. return;
  166. }
  167. var targetType = targetObject.GetType();
  168. var targetTypeName = targetType.Name;
  169. var propertyInfo = targetType.GetRuntimeProperty(propertyName);
  170. if (propertyInfo is null)
  171. {
  172. throw new ArgumentException(string.Format(
  173. CultureInfo.CurrentCulture,
  174. "Cannot find a property named {0} on type {1}.",
  175. propertyName,
  176. targetTypeName));
  177. }
  178. else if (!propertyInfo.CanWrite)
  179. {
  180. throw new ArgumentException(string.Format(
  181. CultureInfo.CurrentCulture,
  182. "Cannot find a property named {0} on type {1}.",
  183. propertyName,
  184. targetTypeName));
  185. }
  186. Exception? innerException = null;
  187. try
  188. {
  189. object? result = null;
  190. var propertyType = propertyInfo.PropertyType;
  191. var propertyTypeInfo = propertyType.GetTypeInfo();
  192. if (Value is null)
  193. {
  194. // The result can be null if the type is generic (nullable), or the default value of the type in question
  195. result = propertyTypeInfo.IsValueType ? Activator.CreateInstance(propertyType) : null;
  196. }
  197. else if (propertyTypeInfo.IsAssignableFrom(Value.GetType().GetTypeInfo()))
  198. {
  199. result = Value;
  200. }
  201. else
  202. {
  203. var valueAsString = Value.ToString();
  204. if (valueAsString is not null)
  205. {
  206. result = propertyTypeInfo.IsEnum ? Enum.Parse(propertyType, valueAsString, false) :
  207. Interactivity.TypeConverterHelper.Convert(valueAsString, propertyType);
  208. }
  209. }
  210. propertyInfo.SetValue(targetObject, result, []);
  211. }
  212. catch (FormatException e)
  213. {
  214. innerException = e;
  215. }
  216. catch (ArgumentException e)
  217. {
  218. innerException = e;
  219. }
  220. if (innerException is not null)
  221. {
  222. throw new ArgumentException(string.Format(
  223. CultureInfo.CurrentCulture,
  224. "Cannot assign value of type {0} to property {1} of type {2}. The {1} property can be assigned only values of type {2}.",
  225. Value?.GetType().Name ?? "null",
  226. propertyName,
  227. propertyInfo.PropertyType.Name),
  228. innerException);
  229. }
  230. }
  231. [RequiresUnreferencedCode("This functionality is not compatible with trimming.")]
  232. private void UpdateAvaloniaPropertyValue(AvaloniaObject avaloniaObject, AvaloniaProperty property)
  233. {
  234. ValidateAvaloniaProperty(property);
  235. Exception? innerException = null;
  236. try
  237. {
  238. object? result = null;
  239. var propertyType = property.PropertyType;
  240. var propertyTypeInfo = propertyType.GetTypeInfo();
  241. if (Value is null)
  242. {
  243. // The result can be null if the type is generic (nullable), or the default value of the type in question
  244. result = propertyTypeInfo.IsValueType ? Activator.CreateInstance(propertyType) : null;
  245. }
  246. else if (propertyTypeInfo.IsAssignableFrom(Value.GetType().GetTypeInfo()))
  247. {
  248. result = Value;
  249. }
  250. else
  251. {
  252. var valueAsString = Value.ToString();
  253. if (valueAsString is not null)
  254. {
  255. result = propertyTypeInfo.IsEnum ? Enum.Parse(propertyType, valueAsString, false) :
  256. Interactivity.TypeConverterHelper.Convert(valueAsString, propertyType);
  257. }
  258. }
  259. avaloniaObject.SetValue(property, result);
  260. }
  261. catch (FormatException e)
  262. {
  263. innerException = e;
  264. }
  265. catch (ArgumentException e)
  266. {
  267. innerException = e;
  268. }
  269. if (innerException is not null)
  270. {
  271. throw new ArgumentException(string.Format(
  272. CultureInfo.CurrentCulture,
  273. "Cannot assign value of type {0} to property {1} of type {2}. The {1} property can be assigned only values of type {2}.",
  274. Value?.GetType().Name ?? "null",
  275. PropertyName,
  276. avaloniaObject.GetType().Name),
  277. innerException);
  278. }
  279. }
  280. /// <summary>
  281. /// Ensures the property is not null and can be written to.
  282. /// </summary>
  283. private void ValidateAvaloniaProperty(AvaloniaProperty? property)
  284. {
  285. if (property is null)
  286. {
  287. throw new ArgumentException(string.Format(
  288. CultureInfo.CurrentCulture,
  289. "Cannot find a property named {0}.",
  290. PropertyName));
  291. }
  292. else if (property.IsReadOnly)
  293. {
  294. throw new ArgumentException(string.Format(
  295. CultureInfo.CurrentCulture,
  296. "Cannot find a property named {0}.",
  297. PropertyName));
  298. }
  299. }
  300. }