TiltEffect.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics.CodeAnalysis;
  4. using Windows.Foundation;
  5. using Windows.UI.Xaml;
  6. using Windows.UI.Xaml.Input;
  7. using Windows.UI.Xaml.Media;
  8. using Windows.UI.Xaml.Media.Animation;
  9. namespace FormulaOneApp.Classic.Windows.Controls.Tile
  10. {
  11. /// <summary>
  12. /// This code provides attached properties for adding a 'tilt' effect to all
  13. /// controls within a container.
  14. /// </summary>
  15. /// <QualityBand>Preview</QualityBand>
  16. [SuppressMessage("Microsoft.Design", "CA1052:StaticHolderTypesShouldBeSealed", Justification = "Cannot be static and derive from DependencyObject.")]
  17. public partial class TiltEffect : DependencyObject
  18. {
  19. /// <summary>
  20. /// Cache of previous cache modes. Not using weak references for now.
  21. /// </summary>
  22. private static Dictionary<DependencyObject, CacheMode> _originalCacheMode = new Dictionary<DependencyObject, CacheMode>();
  23. /// <summary>
  24. /// Maximum amount of tilt, in radians.
  25. /// </summary>
  26. private const double MaxAngle = 0.3;
  27. /// <summary>
  28. /// Maximum amount of depression, in pixels
  29. /// </summary>
  30. private const double MaxDepression = 25;
  31. /// <summary>
  32. /// Delay between releasing an element and the tilt release animation
  33. /// playing.
  34. /// </summary>
  35. private static readonly TimeSpan TiltReturnAnimationDelay = TimeSpan.FromMilliseconds(200);
  36. /// <summary>
  37. /// Duration of tilt release animation.
  38. /// </summary>
  39. private static readonly TimeSpan TiltReturnAnimationDuration = TimeSpan.FromMilliseconds(100);
  40. /// <summary>
  41. /// The control that is currently being tilted.
  42. /// </summary>
  43. private static FrameworkElement currentTiltElement;
  44. /// <summary>
  45. /// The single instance of a storyboard used for all tilts.
  46. /// </summary>
  47. private static Storyboard tiltReturnStoryboard;
  48. /// <summary>
  49. /// The single instance of an X rotation used for all tilts.
  50. /// </summary>
  51. private static DoubleAnimation tiltReturnXAnimation;
  52. /// <summary>
  53. /// The single instance of a Y rotation used for all tilts.
  54. /// </summary>
  55. private static DoubleAnimation tiltReturnYAnimation;
  56. /// <summary>
  57. /// The single instance of a Z depression used for all tilts.
  58. /// </summary>
  59. private static DoubleAnimation tiltReturnZAnimation;
  60. /// <summary>
  61. /// The center of the tilt element.
  62. /// </summary>
  63. private static Point currentTiltElementCenter;
  64. /// <summary>
  65. /// Whether the animation just completed was for a 'pause' or not.
  66. /// </summary>
  67. private static bool wasPauseAnimation = false;
  68. #region Constructor and Static Constructor
  69. /// <summary>
  70. /// This is not a constructable class, but it cannot be static because
  71. /// it derives from DependencyObject.
  72. /// </summary>
  73. private TiltEffect()
  74. {
  75. }
  76. #endregion
  77. #region Dependency properties
  78. /// <summary>
  79. /// Whether the tilt effect is enabled on a container (and all its
  80. /// children).
  81. /// </summary>
  82. public static readonly DependencyProperty IsTiltEnabledProperty = DependencyProperty.RegisterAttached(
  83. "IsTiltEnabled",
  84. typeof(bool),
  85. typeof(TiltEffect),
  86. new PropertyMetadata(false, OnIsTiltEnabledChanged)
  87. );
  88. /// <summary>
  89. /// Gets the IsTiltEnabled dependency property from an object.
  90. /// </summary>
  91. /// <param name="source">The object to get the property from.</param>
  92. /// <returns>The property's value.</returns>
  93. [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
  94. public static bool GetIsTiltEnabled(DependencyObject source)
  95. {
  96. return (bool)source.GetValue(IsTiltEnabledProperty);
  97. }
  98. /// <summary>
  99. /// Sets the IsTiltEnabled dependency property on an object.
  100. /// </summary>
  101. /// <param name="source">The object to set the property on.</param>
  102. /// <param name="value">The value to set.</param>
  103. [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
  104. public static void SetIsTiltEnabled(DependencyObject source, bool value)
  105. {
  106. source.SetValue(IsTiltEnabledProperty, value);
  107. }
  108. /// <summary>
  109. /// Suppresses the tilt effect on a single control that would otherwise
  110. /// be tilted.
  111. /// </summary>
  112. public static readonly DependencyProperty SuppressTiltProperty = DependencyProperty.RegisterAttached(
  113. "SuppressTilt",
  114. typeof(bool),
  115. typeof(TiltEffect),
  116. null
  117. );
  118. /// <summary>
  119. /// Gets the SuppressTilt dependency property from an object.
  120. /// </summary>
  121. /// <param name="source">The object to get the property from.</param>
  122. /// <returns>The property's value.</returns>
  123. [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
  124. public static bool GetSuppressTilt(DependencyObject source)
  125. {
  126. return (bool)source.GetValue(SuppressTiltProperty);
  127. }
  128. /// <summary>
  129. /// Sets the SuppressTilt dependency property from an object.
  130. /// </summary>
  131. /// <param name="source">The object to get the property from.</param>
  132. /// <param name="value">The property's value.</param>
  133. [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Standard pattern.")]
  134. public static void SetSuppressTilt(DependencyObject source, bool value)
  135. {
  136. source.SetValue(SuppressTiltProperty, value);
  137. }
  138. /// <summary>
  139. /// Property change handler for the IsTiltEnabled dependency property.
  140. /// </summary>
  141. /// <param name="target">The element that the property is atteched to.</param>
  142. /// <param name="args">Event arguments.</param>
  143. /// <remarks>
  144. /// Adds or removes event handlers from the element that has been
  145. /// (un)registered for tilting.
  146. /// </remarks>
  147. private static void OnIsTiltEnabledChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
  148. {
  149. FrameworkElement fe = target as FrameworkElement;
  150. if (fe != null)
  151. {
  152. // Add / remove the event handler if necessary
  153. if ((bool)args.NewValue == true)
  154. {
  155. fe.PointerPressed += TiltEffect_PointerPressed;
  156. }
  157. else
  158. {
  159. fe.PointerPressed -= TiltEffect_PointerPressed;
  160. }
  161. }
  162. }
  163. #endregion
  164. #region Top-level manipulation event handlers
  165. /// <summary>
  166. /// Event handler for ManipulationStarted.
  167. /// </summary>
  168. /// <param name="sender">sender of the event - this will be the tilt
  169. /// container (eg, entire page).</param>
  170. /// <param name="e">Event arguments.</param>
  171. private static void TiltEffect_PointerPressed(object sender, PointerRoutedEventArgs e)
  172. {
  173. TryStartTiltEffect(sender as FrameworkElement, e);
  174. }
  175. /// <summary>
  176. /// Event handler for ManipulationDelta
  177. /// </summary>
  178. /// <param name="sender">sender of the event - this will be the tilting
  179. /// object (eg a button).</param>
  180. /// <param name="e">Event arguments.</param>
  181. private static void TiltEffect_PointerMoved(object sender, PointerRoutedEventArgs e)
  182. {
  183. ContinueTiltEffect(sender as FrameworkElement, e);
  184. }
  185. /// <summary>
  186. /// Event handler for ManipulationCompleted.
  187. /// </summary>
  188. /// <param name="sender">sender of the event - this will be the tilting
  189. /// object (eg a button).</param>
  190. /// <param name="e">Event arguments.</param>
  191. private static void TiltEffect_PointerReleased(object sender, PointerRoutedEventArgs e)
  192. {
  193. EndTiltEffect(currentTiltElement);
  194. }
  195. #endregion
  196. #region Core tilt logic
  197. /// <summary>
  198. /// Checks if the manipulation should cause a tilt, and if so starts the
  199. /// tilt effect.
  200. /// </summary>
  201. /// <param name="source">The source of the manipulation (the tilt
  202. /// container, eg entire page).</param>
  203. /// <param name="e">The args from the ManipulationStarted event.</param>
  204. private static void TryStartTiltEffect(FrameworkElement source, PointerRoutedEventArgs e)
  205. {
  206. FrameworkElement element = source; // VisualTreeHelper.GetChild(ancestor, 0) as FrameworkElement;
  207. FrameworkElement container = source;// e.Container as FrameworkElement;
  208. if (element == null || container == null)
  209. return;
  210. // Touch point relative to the element being tilted.
  211. Point tiltTouchPoint = e.GetCurrentPoint(element).Position; // container.TransformToVisual(element).TransformPoint(e.GetCurrentPoint(element));
  212. // Center of the element being tilted.
  213. Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
  214. // Camera adjustment.
  215. Point centerToCenterDelta = GetCenterToCenterDelta(element, source);
  216. BeginTiltEffect(element, tiltTouchPoint, elementCenter, centerToCenterDelta, e.Pointer);
  217. return;
  218. }
  219. /// <summary>
  220. /// Computes the delta between the centre of an element and its
  221. /// container.
  222. /// </summary>
  223. /// <param name="element">The element to compare.</param>
  224. /// <param name="container">The element to compare against.</param>
  225. /// <returns>A point that represents the delta between the two centers.</returns>
  226. private static Point GetCenterToCenterDelta(FrameworkElement element, FrameworkElement container)
  227. {
  228. Point elementCenter = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
  229. Point containerCenter = new Point(container.ActualWidth / 2, container.ActualHeight / 2);
  230. Point transformedElementCenter = element.TransformToVisual(container).TransformPoint(elementCenter);
  231. return new Point(containerCenter.X - transformedElementCenter.X, containerCenter.Y - transformedElementCenter.Y);
  232. }
  233. /// <summary>
  234. /// Begins the tilt effect by preparing the control and doing the
  235. /// initial animation.
  236. /// </summary>
  237. /// <param name="element">The element to tilt.</param>
  238. /// <param name="touchPoint">The touch point, in element coordinates.</param>
  239. /// <param name="centerPoint">The center point of the element in element
  240. /// coordinates.</param>
  241. /// <param name="centerDelta">The delta between the
  242. /// <paramref name="element"/>'s center and the container's center.</param>
  243. private static void BeginTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint, Point centerDelta, Pointer p)
  244. {
  245. if (tiltReturnStoryboard != null)
  246. {
  247. StopTiltReturnStoryboardAndCleanup();
  248. }
  249. if (PrepareControlForTilt(element, centerDelta, p) == false)
  250. {
  251. return;
  252. }
  253. currentTiltElement = element;
  254. currentTiltElementCenter = centerPoint;
  255. PrepareTiltReturnStoryboard(element);
  256. ApplyTiltEffect(currentTiltElement, touchPoint, currentTiltElementCenter);
  257. }
  258. /// <summary>
  259. /// Prepares a control to be tilted by setting up a plane projection and
  260. /// some event handlers.
  261. /// </summary>
  262. /// <param name="element">The control that is to be tilted.</param>
  263. /// <param name="centerDelta">Delta between the element's center and the
  264. /// tilt container's.</param>
  265. /// <returns>true if successful; false otherwise.</returns>
  266. /// <remarks>
  267. /// This method is conservative; it will fail any attempt to tilt a
  268. /// control that already has a projection on it.
  269. /// </remarks>
  270. private static bool PrepareControlForTilt(FrameworkElement element, Point centerDelta, Pointer p)
  271. {
  272. // Prevents interference with any existing transforms
  273. if (element.Projection != null || (element.RenderTransform != null && element.RenderTransform.GetType() != typeof(MatrixTransform)))
  274. {
  275. return false;
  276. }
  277. _originalCacheMode[element] = element.CacheMode;
  278. element.CacheMode = new BitmapCache();
  279. TranslateTransform transform = new TranslateTransform();
  280. transform.X = centerDelta.X;
  281. transform.Y = centerDelta.Y;
  282. element.RenderTransform = transform;
  283. PlaneProjection projection = new PlaneProjection();
  284. projection.GlobalOffsetX = -1 * centerDelta.X;
  285. projection.GlobalOffsetY = -1 * centerDelta.Y;
  286. element.Projection = projection;
  287. element.PointerMoved += TiltEffect_PointerMoved;
  288. element.PointerReleased += TiltEffect_PointerReleased;
  289. element.CapturePointer(p);
  290. return true;
  291. }
  292. /// <summary>
  293. /// Removes modifications made by PrepareControlForTilt.
  294. /// </summary>
  295. /// <param name="element">THe control to be un-prepared.</param>
  296. /// <remarks>
  297. /// This method is basic; it does not do anything to detect if the
  298. /// control being un-prepared was previously prepared.
  299. /// </remarks>
  300. private static void RevertPrepareControlForTilt(FrameworkElement element)
  301. {
  302. element.PointerMoved -= TiltEffect_PointerMoved;
  303. element.PointerReleased -= TiltEffect_PointerReleased;
  304. element.Projection = null;
  305. element.RenderTransform = null;
  306. CacheMode original;
  307. if (_originalCacheMode.TryGetValue(element, out original))
  308. {
  309. element.CacheMode = original;
  310. _originalCacheMode.Remove(element);
  311. }
  312. else
  313. {
  314. element.CacheMode = null;
  315. }
  316. }
  317. /// <summary>
  318. /// Creates the tilt return storyboard (if not already created) and
  319. /// targets it to the projection.
  320. /// </summary>
  321. /// <param name="element">The framework element to prepare for
  322. /// projection.</param>
  323. private static void PrepareTiltReturnStoryboard(FrameworkElement element)
  324. {
  325. if (tiltReturnStoryboard == null)
  326. {
  327. tiltReturnStoryboard = new Storyboard();
  328. tiltReturnStoryboard.Completed += TiltReturnStoryboard_Completed;
  329. tiltReturnXAnimation = new DoubleAnimation();
  330. Storyboard.SetTargetProperty(tiltReturnXAnimation, "RotationX");
  331. tiltReturnXAnimation.BeginTime = TiltReturnAnimationDelay;
  332. tiltReturnXAnimation.To = 0;
  333. tiltReturnXAnimation.Duration = TiltReturnAnimationDuration;
  334. tiltReturnYAnimation = new DoubleAnimation();
  335. Storyboard.SetTargetProperty(tiltReturnYAnimation, "RotationY");
  336. tiltReturnYAnimation.BeginTime = TiltReturnAnimationDelay;
  337. tiltReturnYAnimation.To = 0;
  338. tiltReturnYAnimation.Duration = TiltReturnAnimationDuration;
  339. tiltReturnZAnimation = new DoubleAnimation();
  340. Storyboard.SetTargetProperty(tiltReturnZAnimation, "GlobalOffsetZ");
  341. tiltReturnZAnimation.BeginTime = TiltReturnAnimationDelay;
  342. tiltReturnZAnimation.To = 0;
  343. tiltReturnZAnimation.Duration = TiltReturnAnimationDuration;
  344. tiltReturnStoryboard.Children.Add(tiltReturnXAnimation);
  345. tiltReturnStoryboard.Children.Add(tiltReturnYAnimation);
  346. tiltReturnStoryboard.Children.Add(tiltReturnZAnimation);
  347. }
  348. Storyboard.SetTarget(tiltReturnXAnimation, element.Projection);
  349. Storyboard.SetTarget(tiltReturnYAnimation, element.Projection);
  350. Storyboard.SetTarget(tiltReturnZAnimation, element.Projection);
  351. }
  352. /// <summary>
  353. /// Continues a tilt effect that is currently applied to an element,
  354. /// presumably because the user moved their finger.
  355. /// </summary>
  356. /// <param name="element">The element being tilted.</param>
  357. /// <param name="e">The manipulation event args.</param>
  358. private static void ContinueTiltEffect(FrameworkElement element, PointerRoutedEventArgs e)
  359. {
  360. FrameworkElement container = element;
  361. if (container == null || element == null)
  362. {
  363. return;
  364. }
  365. Point tiltTouchPoint = e.GetCurrentPoint(element).Position;
  366. // If touch moved outside bounds of element, then pause the tilt
  367. // (but don't cancel it)
  368. if (new Rect(0, 0, currentTiltElement.ActualWidth, currentTiltElement.ActualHeight).Contains(tiltTouchPoint) != true)
  369. {
  370. PauseTiltEffect();
  371. }
  372. else
  373. {
  374. // Apply the updated tilt effect
  375. ApplyTiltEffect(currentTiltElement, tiltTouchPoint, currentTiltElementCenter);
  376. }
  377. }
  378. /// <summary>
  379. /// Ends the tilt effect by playing the animation.
  380. /// </summary>
  381. /// <param name="element">The element being tilted.</param>
  382. private static void EndTiltEffect(FrameworkElement element)
  383. {
  384. if (element != null)
  385. {
  386. element.PointerReleased -= TiltEffect_PointerPressed;
  387. element.PointerMoved -= TiltEffect_PointerMoved;
  388. }
  389. if (tiltReturnStoryboard != null)
  390. {
  391. wasPauseAnimation = false;
  392. if (tiltReturnStoryboard.GetCurrentState() != ClockState.Active)
  393. {
  394. tiltReturnStoryboard.Begin();
  395. }
  396. }
  397. else
  398. {
  399. StopTiltReturnStoryboardAndCleanup();
  400. }
  401. }
  402. /// <summary>
  403. /// Handler for the storyboard complete event.
  404. /// </summary>
  405. /// <param name="sender">sender of the event.</param>
  406. /// <param name="e">event args.</param>
  407. private static void TiltReturnStoryboard_Completed(object sender, object e)
  408. {
  409. if (wasPauseAnimation)
  410. {
  411. ResetTiltEffect(currentTiltElement);
  412. }
  413. else
  414. {
  415. StopTiltReturnStoryboardAndCleanup();
  416. }
  417. }
  418. /// <summary>
  419. /// Resets the tilt effect on the control, making it appear 'normal'
  420. /// again.
  421. /// </summary>
  422. /// <param name="element">The element to reset the tilt on.</param>
  423. /// <remarks>
  424. /// This method doesn't turn off the tilt effect or cancel any current
  425. /// manipulation; it just temporarily cancels the effect.
  426. /// </remarks>
  427. private static void ResetTiltEffect(FrameworkElement element)
  428. {
  429. PlaneProjection projection = element.Projection as PlaneProjection;
  430. projection.RotationY = 0;
  431. projection.RotationX = 0;
  432. projection.GlobalOffsetZ = 0;
  433. }
  434. /// <summary>
  435. /// Stops the tilt effect and release resources applied to the currently
  436. /// tilted control.
  437. /// </summary>
  438. private static void StopTiltReturnStoryboardAndCleanup()
  439. {
  440. if (tiltReturnStoryboard != null)
  441. {
  442. tiltReturnStoryboard.Stop();
  443. }
  444. RevertPrepareControlForTilt(currentTiltElement);
  445. }
  446. /// <summary>
  447. /// Pauses the tilt effect so that the control returns to the 'at rest'
  448. /// position, but doesn't stop the tilt effect (handlers are still
  449. /// attached).
  450. /// </summary>
  451. private static void PauseTiltEffect()
  452. {
  453. if ((tiltReturnStoryboard != null) && !wasPauseAnimation)
  454. {
  455. tiltReturnStoryboard.Stop();
  456. wasPauseAnimation = true;
  457. tiltReturnStoryboard.Begin();
  458. }
  459. }
  460. /// <summary>
  461. /// Resets the storyboard to not running.
  462. /// </summary>
  463. private static void ResetTiltReturnStoryboard()
  464. {
  465. tiltReturnStoryboard.Stop();
  466. wasPauseAnimation = false;
  467. }
  468. /// <summary>
  469. /// Applies the tilt effect to the control.
  470. /// </summary>
  471. /// <param name="element">the control to tilt.</param>
  472. /// <param name="touchPoint">The touch point, in the container's
  473. /// coordinates.</param>
  474. /// <param name="centerPoint">The center point of the container.</param>
  475. private static void ApplyTiltEffect(FrameworkElement element, Point touchPoint, Point centerPoint)
  476. {
  477. // Stop any active animation
  478. ResetTiltReturnStoryboard();
  479. // Get relative point of the touch in percentage of container size
  480. Point normalizedPoint = new Point(
  481. Math.Min(Math.Max(touchPoint.X / (centerPoint.X * 2), 0), 1),
  482. Math.Min(Math.Max(touchPoint.Y / (centerPoint.Y * 2), 0), 1));
  483. if (double.IsNaN(normalizedPoint.X) || double.IsNaN(normalizedPoint.Y))
  484. {
  485. return;
  486. }
  487. // Shell values
  488. double xMagnitude = Math.Abs(normalizedPoint.X - 0.5);
  489. double yMagnitude = Math.Abs(normalizedPoint.Y - 0.5);
  490. double xDirection = -Math.Sign(normalizedPoint.X - 0.5);
  491. double yDirection = Math.Sign(normalizedPoint.Y - 0.5);
  492. double angleMagnitude = xMagnitude + yMagnitude;
  493. double xAngleContribution = xMagnitude + yMagnitude > 0 ? xMagnitude / (xMagnitude + yMagnitude) : 0;
  494. double angle = angleMagnitude * MaxAngle * 180 / Math.PI;
  495. double depression = (1 - angleMagnitude) * MaxDepression;
  496. // RotationX and RotationY are the angles of rotations about the x-
  497. // or y-*axis*; to achieve a rotation in the x- or y-*direction*, we
  498. // need to swap the two. That is, a rotation to the left about the
  499. // y-axis is a rotation to the left in the x-direction, and a
  500. // rotation up about the x-axis is a rotation up in the y-direction.
  501. PlaneProjection projection = element.Projection as PlaneProjection;
  502. projection.RotationY = angle * xAngleContribution * xDirection;
  503. projection.RotationX = angle * (1 - xAngleContribution) * yDirection;
  504. projection.GlobalOffsetZ = -depression;
  505. }
  506. #endregion
  507. }
  508. /// <summary>
  509. /// Couple of simple helpers for walking the visual tree.
  510. /// </summary>
  511. static class TreeHelpers
  512. {
  513. /// <summary>
  514. /// Gets the ancestors of the element, up to the root.
  515. /// </summary>
  516. /// <param name="node">The element to start from.</param>
  517. /// <returns>An enumerator of the ancestors.</returns>
  518. public static IEnumerable<FrameworkElement> GetVisualAncestors(this FrameworkElement node)
  519. {
  520. FrameworkElement parent = node.GetVisualParent();
  521. while (parent != null)
  522. {
  523. yield return parent;
  524. parent = parent.GetVisualParent();
  525. }
  526. }
  527. /// <summary>
  528. /// Gets the visual parent of the element.
  529. /// </summary>
  530. /// <param name="node">The element to check.</param>
  531. /// <returns>The visual parent.</returns>
  532. public static FrameworkElement GetVisualParent(this FrameworkElement node)
  533. {
  534. return VisualTreeHelper.GetParent(node) as FrameworkElement;
  535. }
  536. }
  537. }