diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind
index deba75d104e..2ba6632624a 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsCode.bind
@@ -5,15 +5,49 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
+ xmlns:interactions="using:Microsoft.Xaml.Interactions.Core"
+ xmlns:ani="using:Microsoft.Toolkit.Uwp.UI.Animations"
+ xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Behaviors"
mc:Ignorable="d">
-
+
+
+
-
+ ui:VisualExtensions.NormalizedCenterPoint="0.5"
+ ui:VisualExtensions.Translation="20,12,0"/>
+
+
+
+
diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml
index 50f0f87c579..a8bf12a5a4f 100644
--- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml
+++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Visual Extensions/VisualExtensionsPage.xaml
@@ -3,11 +3,15 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
+ xmlns:interactions="using:Microsoft.Xaml.Interactions.Core"
+ xmlns:ani="using:Microsoft.Toolkit.Uwp.UI.Animations"
+ xmlns:behaviors="using:Microsoft.Toolkit.Uwp.UI.Behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
-
+
-
+ ui:VisualExtensions.NormalizedCenterPoint="0.5"
+ ui:VisualExtensions.Translation="20,12,0"/>
+
+
diff --git a/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs b/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs
index c4f2c57b366..cf1ddb360e6 100644
--- a/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs
+++ b/Microsoft.Toolkit.Uwp.UI/Extensions/VisualExtensions.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Globalization;
using System.Numerics;
using Windows.UI.Composition;
using Windows.UI.Xaml;
@@ -114,6 +115,36 @@ public static void SetOffset(DependencyObject obj, string value)
obj.SetValue(OffsetProperty, value);
}
+ ///
+ /// Gets the "Translation" property of the underlying object for a , in form.
+ ///
+ /// The instance.
+ /// The representation of the "Translation" property property.
+ public static string GetTranslation(DependencyObject obj)
+ {
+ if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode && obj is UIElement element)
+ {
+ return GetTranslationForElement(element);
+ }
+
+ return (string)obj.GetValue(TranslationProperty);
+ }
+
+ ///
+ /// Sets the "Translation" property of the underlying object for a , in form.
+ ///
+ /// The instance.
+ /// The representation of the "Translation" property property to be set.
+ public static void SetTranslation(DependencyObject obj, string value)
+ {
+ if (!DesignTimeHelpers.IsRunningInLegacyDesignerMode && obj is UIElement element)
+ {
+ SetTranslationForElement(value, element);
+ }
+
+ obj.SetValue(TranslationProperty, value);
+ }
+
///
/// Gets the of a UIElement
///
@@ -334,6 +365,12 @@ public static void SetNormalizedCenterPoint(DependencyObject obj, string value)
public static readonly DependencyProperty OffsetProperty =
DependencyProperty.RegisterAttached("Offset", typeof(string), typeof(VisualExtensions), new PropertyMetadata(null, OnOffsetChanged));
+ ///
+ /// Identifies the Translation attached property.
+ ///
+ public static readonly DependencyProperty TranslationProperty =
+ DependencyProperty.RegisterAttached("Translation", typeof(string), typeof(VisualExtensions), new PropertyMetadata(null, OnTranslationChanged));
+
///
/// Identifies the Opacity attached property.
///
@@ -400,6 +437,14 @@ private static void OnOffsetChanged(DependencyObject d, DependencyPropertyChange
}
}
+ private static void OnTranslationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (e.NewValue is string str)
+ {
+ SetTranslation(d, str);
+ }
+ }
+
private static void OnOpacityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue is double dbl)
@@ -503,6 +548,37 @@ private static void SetOffsetForElement(string value, UIElement element)
visual.Offset = value.ToVector3();
}
+ private static string GetTranslationForElement(UIElement element)
+ {
+ CompositionGetValueStatus result = GetVisual(element).Properties.TryGetVector3("Translation", out Vector3 translation);
+
+ return result switch
+ {
+ // The ("G", CultureInfo.InvariantCulture) combination produces a string with the default numeric
+ // formatting style, and using ',' as component separator, so that the resulting text can safely
+ // be parsed back if needed with the StringExtensions.ToVector3(string) extension, which uses
+ // the invariant culture mode by default so that the syntax will always match that from XAML.
+ CompositionGetValueStatus.Succeeded => translation.ToString("G", CultureInfo.InvariantCulture),
+ _ => "<0, 0, 0>"
+ };
+ }
+
+ private static void SetTranslationForElement(string value, UIElement element)
+ {
+ ElementCompositionPreview.SetIsTranslationEnabled(element, true);
+
+ // The "Translation" attached property refers to the "hidden" property that is enabled
+ // through "ElementCompositionPreview.SetIsTranslationEnabled". The value for this property
+ // is not available directly on the Visual class and can only be accessed through its property
+ // set. Note that this "Translation" value is not the same as Visual.TransformMatrix.Translation.
+ // In fact, the latter doesn't require to be explicitly enabled and is actually combined with
+ // this at runtime (ie. the whole transform matrix is combined with the additional translation
+ // from the "Translation" property, if any), and the two can be set and animated independently.
+ // In this case we're just interested in the "Translation" property, which is more commonly used
+ // as it can also be animated directly with a Vector3 animation instead of a Matrix4x4 one.
+ GetVisual(element).Properties.InsertVector3("Translation", value.ToVector3());
+ }
+
private static double GetOpacityForElement(UIElement element)
{
var visual = GetVisual(element);
diff --git a/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs b/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs
new file mode 100644
index 00000000000..244cfaba0b3
--- /dev/null
+++ b/UnitTests/UnitTests.UWP/UI/Extensions/Test_VisualExtensions.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.Toolkit.Uwp;
+using Windows.UI.Xaml.Controls;
+using Microsoft.Toolkit.Uwp.UI;
+using System.Numerics;
+using Windows.UI.Composition;
+using Microsoft.Toolkit.Uwp.UI.Animations;
+
+namespace UnitTests.UWP.UI
+{
+ [TestClass]
+ [TestCategory("Test_VisualExtensions")]
+ public class Test_VisualExtensions : VisualUITestBase
+ {
+ [TestMethod]
+ public async Task GetDefaultTranslation()
+ {
+ await App.DispatcherQueue.EnqueueAsync(() =>
+ {
+ var button = new Button();
+
+ string text = VisualExtensions.GetTranslation(button);
+
+ Assert.AreEqual(text, "<0, 0, 0>");
+ });
+ }
+
+ [TestMethod]
+ public async Task SetAndGetTranslation()
+ {
+ await App.DispatcherQueue.EnqueueAsync(async () =>
+ {
+ var button = new Button();
+ var grid = new Grid() { Children = { button } };
+
+ VisualExtensions.SetTranslation(button, "80, 20, 0");
+
+ await SetTestContentAsync(grid);
+
+ var success = button.GetVisual().Properties.TryGetVector3("Translation", out Vector3 translation);
+
+ Assert.AreEqual(success, CompositionGetValueStatus.Succeeded);
+ Assert.AreEqual(translation, new Vector3(80, 20, 0));
+
+ string text = VisualExtensions.GetTranslation(button);
+
+ Assert.AreEqual(text, new Vector3(80, 20, 0).ToString());
+ });
+ }
+
+ [TestMethod]
+ public async Task SetAndAnimateTranslation()
+ {
+ await App.DispatcherQueue.EnqueueAsync(async () =>
+ {
+ var button = new Button();
+ var grid = new Grid() { Children = { button } };
+
+ VisualExtensions.SetTranslation(button, "80, 20, 0");
+
+ await SetTestContentAsync(grid);
+
+ await AnimationBuilder.Create()
+ .Translation(to: new Vector3(11, 22, 0))
+ .StartAsync(button);
+
+ var success = button.GetVisual().Properties.TryGetVector3("Translation", out Vector3 translation);
+
+ Assert.AreEqual(success, CompositionGetValueStatus.Succeeded);
+ Assert.AreEqual(translation, new Vector3(11, 22, 0));
+
+ string text = VisualExtensions.GetTranslation(button);
+
+ Assert.AreEqual(text, new Vector3(11, 22, 0).ToString());
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
index ff55da9ad64..d03e8e97983 100644
--- a/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
+++ b/UnitTests/UnitTests.UWP/UnitTests.UWP.csproj
@@ -211,6 +211,7 @@
+