From 9b0c70128d6c203db7dd19a87eb7c9329d25859b Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Thu, 13 Apr 2023 17:18:18 -0500 Subject: [PATCH 01/13] Fixed invalid paths in Build.props --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 22592686..e84caab3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,9 +2,9 @@ CommunityToolkit $(MSBuildThisFileDirectory) - $(RepositoryDirectory)\tooling + $(RepositoryDirectory)tooling true - $(RepositoryDirectory)\components\Extensions\src\CommunityToolkit.WinUI.Extensions.csproj + $(RepositoryDirectory)components\Extensions\src\CommunityToolkit.WinUI.Extensions.csproj From 204c9448efb48d03d1205f8bd47d5913f7b751d8 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 14 Apr 2023 13:17:53 -0500 Subject: [PATCH 02/13] Initial port of Behaviors. Needs docs and samples. --- components/Behaviors/OpenSolution.bat | 3 + .../samples/Behaviors.Samples.csproj | 8 + components/Behaviors/samples/Behaviors.md | 25 +++ .../Behaviors/samples/Dependencies.props | 31 +++ .../Behaviors/src/AdditionalAssemblyInfo.cs | 13 ++ .../Behaviors/src/ApiInformationHelper.cs | 13 ++ components/Behaviors/src/BehaviorBase.cs | 166 ++++++++++++++ .../CommunityToolkit.WinUI.Behaviors.csproj | 14 ++ components/Behaviors/src/Dependencies.props | 35 +++ components/Behaviors/src/FocusBehavior.cs | 207 ++++++++++++++++++ components/Behaviors/src/FocusTarget.cs | 42 ++++ components/Behaviors/src/FocusTargetList.cs | 12 + .../src/Keyboard/KeyDownTriggerBehavior.cs | 60 +++++ components/Behaviors/src/MultiTarget.props | 9 + .../src/Select/AutoSelectBehavior.cs | 15 ++ .../Viewport/ViewportBehavior.Properties.cs | 103 +++++++++ .../src/Viewport/ViewportBehavior.cs | 125 +++++++++++ .../Behaviors/tests/Behaviors.Tests.projitems | 23 ++ .../Behaviors/tests/Behaviors.Tests.shproj | 13 ++ .../tests/ExampleBehaviorsTestClass.cs | 41 ++++ .../tests/ExampleBehaviorsTestPage.xaml | 11 + .../tests/ExampleBehaviorsTestPage.xaml.cs | 16 ++ 22 files changed, 985 insertions(+) create mode 100644 components/Behaviors/OpenSolution.bat create mode 100644 components/Behaviors/samples/Behaviors.Samples.csproj create mode 100644 components/Behaviors/samples/Behaviors.md create mode 100644 components/Behaviors/samples/Dependencies.props create mode 100644 components/Behaviors/src/AdditionalAssemblyInfo.cs create mode 100644 components/Behaviors/src/ApiInformationHelper.cs create mode 100644 components/Behaviors/src/BehaviorBase.cs create mode 100644 components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj create mode 100644 components/Behaviors/src/Dependencies.props create mode 100644 components/Behaviors/src/FocusBehavior.cs create mode 100644 components/Behaviors/src/FocusTarget.cs create mode 100644 components/Behaviors/src/FocusTargetList.cs create mode 100644 components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs create mode 100644 components/Behaviors/src/MultiTarget.props create mode 100644 components/Behaviors/src/Select/AutoSelectBehavior.cs create mode 100644 components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs create mode 100644 components/Behaviors/src/Viewport/ViewportBehavior.cs create mode 100644 components/Behaviors/tests/Behaviors.Tests.projitems create mode 100644 components/Behaviors/tests/Behaviors.Tests.shproj create mode 100644 components/Behaviors/tests/ExampleBehaviorsTestClass.cs create mode 100644 components/Behaviors/tests/ExampleBehaviorsTestPage.xaml create mode 100644 components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs diff --git a/components/Behaviors/OpenSolution.bat b/components/Behaviors/OpenSolution.bat new file mode 100644 index 00000000..814a56d4 --- /dev/null +++ b/components/Behaviors/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/Behaviors/samples/Behaviors.Samples.csproj b/components/Behaviors/samples/Behaviors.Samples.csproj new file mode 100644 index 00000000..95f546a6 --- /dev/null +++ b/components/Behaviors/samples/Behaviors.Samples.csproj @@ -0,0 +1,8 @@ + + + Behaviors + + + + + diff --git a/components/Behaviors/samples/Behaviors.md b/components/Behaviors/samples/Behaviors.md new file mode 100644 index 00000000..1345df4f --- /dev/null +++ b/components/Behaviors/samples/Behaviors.md @@ -0,0 +1,25 @@ +--- +title: Behaviors +author: Arlodotexe +description: +keywords: Behaviors +dev_langs: + - csharp +category: Xaml +subcategory: Behaviors +discussion-id: 0 +issue-id: 0 +--- + +# Behaviors + +Xaml Behaviors are an easy way to add common and reusable interactivity to your app with minimal code. + +TODO: +- Simple overview of behaviors +- Link to official Xaml Behavior docs +- Document / create samples for: + - KeyDownTriggerBehavior + - AutoSelectBehavior + - ViewPortBehavior + - FocusBehavior diff --git a/components/Behaviors/samples/Dependencies.props b/components/Behaviors/samples/Dependencies.props new file mode 100644 index 00000000..e622e1df --- /dev/null +++ b/components/Behaviors/samples/Dependencies.props @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Behaviors/src/AdditionalAssemblyInfo.cs b/components/Behaviors/src/AdditionalAssemblyInfo.cs new file mode 100644 index 00000000..a909d34f --- /dev/null +++ b/components/Behaviors/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// 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.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("Behaviors.Tests.Uwp")] +[assembly: InternalsVisibleTo("Behaviors.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/Behaviors/src/ApiInformationHelper.cs b/components/Behaviors/src/ApiInformationHelper.cs new file mode 100644 index 00000000..15fed176 --- /dev/null +++ b/components/Behaviors/src/ApiInformationHelper.cs @@ -0,0 +1,13 @@ +// 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 Windows.Foundation.Metadata; + +namespace CommunityToolkit.WinUI.Behaviors; + +internal class ApiInformationHelper +{ + // 1903 - 18362 + public static bool IsXamlRootAvailable { get; } = ApiInformation.IsPropertyPresent("Windows.UI.Xaml.UIElement", "XamlRoot"); +} diff --git a/components/Behaviors/src/BehaviorBase.cs b/components/Behaviors/src/BehaviorBase.cs new file mode 100644 index 00000000..157ab217 --- /dev/null +++ b/components/Behaviors/src/BehaviorBase.cs @@ -0,0 +1,166 @@ +// 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 Microsoft.Xaml.Interactivity; + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// Base class for behaviors that solves 2 problems: +/// 1. Prevent duplicate initialization that can happen (prevent multiple OnAttached calls); +/// 2. Whenever initially fails, this method will subscribe to to allow lazy initialization. +/// +/// The type of the associated object. +/// +/// +/// For more info, see https://github.com/CommunityToolkit/WindowsCommunityToolkit/issues/1008. +/// +public abstract class BehaviorBase : Behavior + where T : UIElement +{ + private bool _isAttaching; + private bool _isAttached; + + /// + /// Gets a value indicating whether this behavior is attached. + /// + /// + /// true if this behavior is attached; otherwise, false. + /// + protected bool IsAttached + { + get { return _isAttached; } + } + + /// + /// Called after the behavior is attached to the . + /// + /// + /// Override this to hook up functionality to the + /// + protected override void OnAttached() + { + base.OnAttached(); + + HandleAttach(); + + var frameworkElement = AssociatedObject as FrameworkElement; + if (frameworkElement != null) + { + frameworkElement.Loaded += OnAssociatedObjectLoaded; + frameworkElement.Unloaded += OnAssociatedObjectUnloaded; + frameworkElement.SizeChanged += OnAssociatedObjectSizeChanged; + } + } + + /// + /// Called when the behavior is being detached from its . + /// + /// + /// Override this to unhook functionality from the + /// + protected override void OnDetaching() + { + base.OnDetaching(); + + var frameworkElement = AssociatedObject as FrameworkElement; + if (frameworkElement != null) + { + frameworkElement.Loaded -= OnAssociatedObjectLoaded; + frameworkElement.Unloaded -= OnAssociatedObjectUnloaded; + frameworkElement.SizeChanged -= OnAssociatedObjectSizeChanged; + } + + HandleDetach(); + } + + /// + /// Called when the associated object has been loaded. + /// + protected virtual void OnAssociatedObjectLoaded() + { + } + + /// + /// Called when the associated object has been unloaded. + /// + protected virtual void OnAssociatedObjectUnloaded() + { + } + + /// + /// Initializes the behavior to the associated object. + /// + /// true if the initialization succeeded; otherwise false. + protected virtual bool Initialize() + { + return true; + } + + /// + /// Uninitializes the behavior from the associated object. + /// + /// true if uninitialization succeeded; otherwise false. + protected virtual bool Uninitialize() + { + return true; + } + + private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e) + { + if (!_isAttached) + { + HandleAttach(); + } + + OnAssociatedObjectLoaded(); + } + + private void OnAssociatedObjectUnloaded(object sender, RoutedEventArgs e) + { + OnAssociatedObjectUnloaded(); + + // Note: don't detach here, we'll let the behavior implementation take care of that + } + + private void OnAssociatedObjectSizeChanged(object sender, SizeChangedEventArgs e) + { + if (!_isAttached) + { + HandleAttach(); + } + } + + private void HandleAttach() + { + if (_isAttaching || _isAttached) + { + return; + } + + _isAttaching = true; + + var attached = Initialize(); + if (attached) + { + _isAttached = true; + } + + _isAttaching = false; + } + + private void HandleDetach() + { + if (!_isAttached) + { + return; + } + + var detached = Uninitialize(); + if (detached) + { + _isAttached = false; + } + } +} diff --git a/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj b/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj new file mode 100644 index 00000000..935a777b --- /dev/null +++ b/components/Behaviors/src/CommunityToolkit.WinUI.Behaviors.csproj @@ -0,0 +1,14 @@ + + + Behaviors + This package contains Behaviors. + 8.0.0-beta.1 + + + CommunityToolkit.WinUI.Behaviors + $(PackageId)Rns + + + + + diff --git a/components/Behaviors/src/Dependencies.props b/components/Behaviors/src/Dependencies.props new file mode 100644 index 00000000..6b1bf6d2 --- /dev/null +++ b/components/Behaviors/src/Dependencies.props @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/Behaviors/src/FocusBehavior.cs b/components/Behaviors/src/FocusBehavior.cs new file mode 100644 index 00000000..1424b379 --- /dev/null +++ b/components/Behaviors/src/FocusBehavior.cs @@ -0,0 +1,207 @@ +// 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. + +#if WINDOWS_UWP || HAS_UNO && WINUI2 +using Windows.System; +#elif WINDOWS_WINAPPSDK || HAS_UNO && WINUI3 +using Microsoft.UI.Dispatching; +#endif + +namespace CommunityToolkit.WinUI.Behaviors; + +#pragma warning disable SA1402 // File may only contain a single type +/// +/// This behavior sets the focus on the first control of which accepts it. +/// The focus will be set following the order. The first control being ready +/// and accepting the focus will receive it. +/// The focus can be set to another control with a higher priority if it loads before . +/// The focus can be set to another control if some controls will be loaded/unloaded later. +/// +[ContentProperty(Name = nameof(Targets))] +public sealed class FocusBehavior : BehaviorBase +{ + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty TargetsProperty = DependencyProperty.Register( + nameof(Targets), + typeof(FocusTargetList), + typeof(FocusBehavior), + new PropertyMetadata(null, OnTargetsPropertyChanged)); + + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty FocusEngagementTimeoutProperty = DependencyProperty.Register( + nameof(FocusEngagementTimeout), + typeof(TimeSpan), + typeof(FocusBehavior), + new PropertyMetadata(TimeSpan.FromMilliseconds(100))); + + private DispatcherQueueTimer? _timer; + + /// + /// Initializes a new instance of the class. + /// + public FocusBehavior() => Targets = new FocusTargetList(); + + /// + /// Gets or sets the ordered list of controls which should receive the focus when the associated object is loaded. + /// + public FocusTargetList Targets + { + get => (FocusTargetList)GetValue(TargetsProperty); + set => SetValue(TargetsProperty, value); + } + + /// + /// Gets or sets the timeout before the stops trying to set the focus to a control with + /// a higher priority. + /// + public TimeSpan FocusEngagementTimeout + { + get => (TimeSpan)GetValue(FocusEngagementTimeoutProperty); + set => SetValue(FocusEngagementTimeoutProperty, value); + } + + /// + protected override void OnAssociatedObjectLoaded() + { + foreach (var target in Targets) + { + target.ControlChanged += OnTargetControlChanged; + } + + ApplyFocus(); + } + + /// + protected override bool Uninitialize() + { + foreach (var target in Targets) + { + target.ControlChanged -= OnTargetControlChanged; + } + + Stop(); + return true; + } + + private static void OnTargetsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var behavior = (FocusBehavior)d; + + if (e.OldValue is FocusTargetList oldTargets) + { + behavior.Stop(oldTargets); + } + + behavior.ApplyFocus(); + } + + private void ApplyFocus() + { + if (Targets.Count == 0) + { + return; + } + + var focusedControlIndex = -1; + var hasListViewBaseControl = false; + for (var i = 0; i < Targets.Count; i++) + { + var control = Targets[i].Control; + if (control is null) + { + continue; + } + + if (control.IsLoaded) + { + if (control.Focus(FocusState.Programmatic)) + { + focusedControlIndex = i; + break; + } + + if (control is ListViewBase listViewBase) + { + // The list may not have any item yet, we wait until the first item is rendered. + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + listViewBase.ContainerContentChanging += OnContainerContentChanging; + hasListViewBaseControl = true; + } + } + else + { + control.Loaded -= OnControlLoaded; + control.Loaded += OnControlLoaded; + } + } + + if (focusedControlIndex == 0 || (!hasListViewBaseControl && Targets.All(t => t.Control?.IsLoaded == true))) + { + // The first control has received the focus or all the control are loaded and none can take the focus: we stop. + Stop(); + } + else if (focusedControlIndex > 0) + { + // We have been able to set the focus on one control. + // We start the timer to detect if we can focus another control with an higher priority. + // This allows us to handle the case where the controls are not loaded in the order we expect. + if (_timer is null) + { +#if !WINAPPSDK + _timer = DispatcherQueue.GetForCurrentThread().CreateTimer(); +#else + _timer = DispatcherQueue.CreateTimer(); +#endif + + _timer.Interval = FocusEngagementTimeout; + _timer.Tick += OnEngagementTimerTick; + _timer.Start(); + } + } + } + + private void OnEngagementTimerTick(object? sender, object e) + { + ApplyFocus(); + Stop(); + } + + private void OnControlLoaded(object? sender, RoutedEventArgs e) => ApplyFocus(); + + private void OnTargetControlChanged(object? sender, EventArgs e) => ApplyFocus(); + + private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args) + { + sender.ContainerContentChanging -= OnContainerContentChanging; + ApplyFocus(); + } + + private void Stop(FocusTargetList? targets = null) + { + if (_timer != null) + { + _timer.Stop(); + _timer = null; + } + + foreach (var target in targets ?? Targets) + { + if (target.Control is null) + { + continue; + } + + target.Control.Loaded -= OnControlLoaded; + + if (target.Control is ListViewBase listViewBase) + { + listViewBase.ContainerContentChanging -= OnContainerContentChanging; + } + } + } +} diff --git a/components/Behaviors/src/FocusTarget.cs b/components/Behaviors/src/FocusTarget.cs new file mode 100644 index 00000000..85249361 --- /dev/null +++ b/components/Behaviors/src/FocusTarget.cs @@ -0,0 +1,42 @@ +// 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. + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// A target for the . +/// +public sealed partial class FocusTarget : DependencyObject +{ + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty ControlProperty = DependencyProperty.Register( + nameof(Control), + typeof(Control), + typeof(FocusTarget), + new PropertyMetadata(null, OnControlChanged)); + + /// + /// Raised when property changed. + /// It can change if we use x:Load on the control. + /// + public event EventHandler? ControlChanged; + + /// + /// Gets or sets the control that will receive the focus. + /// + public Control Control + { + get => (Control)GetValue(ControlProperty); + set => SetValue(ControlProperty, value); + } + + private static void OnControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var target = (FocusTarget)d; + target.ControlChanged?.Invoke(target, EventArgs.Empty); + } +} +#pragma warning restore SA1402 // File may only contain a single type diff --git a/components/Behaviors/src/FocusTargetList.cs b/components/Behaviors/src/FocusTargetList.cs new file mode 100644 index 00000000..65ae0778 --- /dev/null +++ b/components/Behaviors/src/FocusTargetList.cs @@ -0,0 +1,12 @@ +// 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. + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// A collection of . +/// +public sealed class FocusTargetList : List +{ +} diff --git a/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs new file mode 100644 index 00000000..2eddac0c --- /dev/null +++ b/components/Behaviors/src/Keyboard/KeyDownTriggerBehavior.cs @@ -0,0 +1,60 @@ +// 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 Microsoft.Xaml.Interactivity; +using Windows.System; + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// This behavior listens to a key down event on the associated when it is loaded and executes an action. +/// +[TypeConstraint(typeof(FrameworkElement))] +public class KeyDownTriggerBehavior : Trigger +{ + /// + /// Identifies the property. + /// + public static readonly DependencyProperty KeyProperty = DependencyProperty.Register( + nameof(Key), + typeof(VirtualKey), + typeof(KeyDownTriggerBehavior), + new PropertyMetadata(null)); + + /// + /// Gets or sets the key to listen when the associated object is loaded. + /// + public VirtualKey Key + { + get => (VirtualKey)GetValue(KeyProperty); + set => SetValue(KeyProperty, value); + } + + /// + protected override void OnAttached() + { + AssociatedObject.KeyDown += OnAssociatedObjectKeyDown; + } + + /// + protected override void OnDetaching() + { + AssociatedObject.KeyDown -= OnAssociatedObjectKeyDown; + } + + /// + /// Invokes the current actions when the is pressed. + /// + /// The source instance. + /// The arguments for the event (unused). + private void OnAssociatedObjectKeyDown(object sender, KeyRoutedEventArgs keyRoutedEventArgs) + { + if (keyRoutedEventArgs.Key == Key) + { + keyRoutedEventArgs.Handled = true; + Interaction.ExecuteActions(sender, Actions, keyRoutedEventArgs); + } + } +} + diff --git a/components/Behaviors/src/MultiTarget.props b/components/Behaviors/src/MultiTarget.props new file mode 100644 index 00000000..b11c1942 --- /dev/null +++ b/components/Behaviors/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android; + + \ No newline at end of file diff --git a/components/Behaviors/src/Select/AutoSelectBehavior.cs b/components/Behaviors/src/Select/AutoSelectBehavior.cs new file mode 100644 index 00000000..136f39de --- /dev/null +++ b/components/Behaviors/src/Select/AutoSelectBehavior.cs @@ -0,0 +1,15 @@ +// 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. + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// This behavior automatically selects the entire content of the associated when it is loaded. +/// +public sealed class AutoSelectBehavior : BehaviorBase +{ + /// + protected override void OnAssociatedObjectLoaded() => AssociatedObject.SelectAll(); +} + diff --git a/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs b/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs new file mode 100644 index 00000000..b134b240 --- /dev/null +++ b/components/Behaviors/src/Viewport/ViewportBehavior.Properties.cs @@ -0,0 +1,103 @@ +// 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 Microsoft.Xaml.Interactivity; + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// A class for listening to an element enter or exit the ScrollViewer viewport +/// +public partial class ViewportBehavior +{ + /// + /// The IsFullyInViewport value of the associated element + /// + public static readonly DependencyProperty IsFullyInViewportProperty = + DependencyProperty.Register(nameof(IsFullyInViewport), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool), OnIsFullyInViewportChanged)); + + /// + /// The IsInViewport value of the associated element + /// + public static readonly DependencyProperty IsInViewportProperty = + DependencyProperty.Register(nameof(IsInViewport), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool), OnIsInViewportChanged)); + + /// + /// The IsAlwaysOn value of the associated element + /// + public static readonly DependencyProperty IsAlwaysOnProperty = + DependencyProperty.Register(nameof(IsAlwaysOn), typeof(bool), typeof(ViewportBehavior), new PropertyMetadata(default(bool))); + + /// + /// Gets or sets a value indicating whether this behavior will remain attached after the associated element enters the viewport. When false, the behavior will remove itself after entering. + /// + public bool IsAlwaysOn + { + get { return (bool)GetValue(IsAlwaysOnProperty); } + set { SetValue(IsAlwaysOnProperty, value); } + } + + /// + /// Gets a value indicating whether associated element is fully in the ScrollViewer viewport + /// + public bool IsFullyInViewport + { + get { return (bool)GetValue(IsFullyInViewportProperty); } + private set { SetValue(IsFullyInViewportProperty, value); } + } + + /// + /// Gets a value indicating whether associated element is in the ScrollViewer viewport + /// + public bool IsInViewport + { + get { return (bool)GetValue(IsInViewportProperty); } + private set { SetValue(IsInViewportProperty, value); } + } + + /// + /// Event tracking when the object is fully within the viewport or not + /// + /// DependencyObject + /// EventArgs + private static void OnIsFullyInViewportChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var obj = (ViewportBehavior)d; + var value = (bool)e.NewValue; + + if (value) + { + obj.EnteredViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty); + + if (!obj.IsAlwaysOn) + { + Interaction.GetBehaviors(obj.AssociatedObject).Remove(obj); + } + } + else + { + obj.ExitingViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty); + } + } + + /// + /// Event tracking the state of the object as it moves into and out of the viewport + /// + /// DependencyObject + /// EventArgs + private static void OnIsInViewportChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var obj = (ViewportBehavior)d; + var value = (bool)e.NewValue; + + if (value) + { + obj.EnteringViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty); + } + else + { + obj.ExitedViewport?.Invoke(obj.AssociatedObject, EventArgs.Empty); + } + } +} diff --git a/components/Behaviors/src/Viewport/ViewportBehavior.cs b/components/Behaviors/src/Viewport/ViewportBehavior.cs new file mode 100644 index 00000000..e071094b --- /dev/null +++ b/components/Behaviors/src/Viewport/ViewportBehavior.cs @@ -0,0 +1,125 @@ +// 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. + +namespace CommunityToolkit.WinUI.Behaviors; + +/// +/// A class for listening to an element enter or exit the ScrollViewer viewport +/// +public partial class ViewportBehavior : BehaviorBase +{ + /// + /// The ScrollViewer hosting this element. + /// + private ScrollViewer? _hostScrollViewer; + + /// + /// Associated element fully enter the ScrollViewer viewport event + /// + public event EventHandler? EnteredViewport; + + /// + /// Associated element fully exit the ScrollViewer viewport event + /// + public event EventHandler? ExitedViewport; + + /// + /// Associated element enter the ScrollViewer viewport event + /// + public event EventHandler? EnteringViewport; + + /// + /// Associated element exit the ScrollViewer viewport event + /// + public event EventHandler? ExitingViewport; + + /// + /// Called after the behavior is attached to the . + /// + /// + /// Override this to hook up functionality to the + /// + protected override void OnAttached() + { + base.OnAttached(); + + var parent = VisualTreeHelper.GetParent(AssociatedObject); + if (parent == null) + { + AssociatedObject.Loaded += AssociatedObject_Loaded; + return; + } + + Init(); + } + + /// + /// Called when the behavior is being detached from its . + /// + /// + /// Override this to unhook functionality from the + /// + protected override void OnDetaching() + { + base.OnDetaching(); + + if (_hostScrollViewer is not null) + { + _hostScrollViewer.ViewChanged -= ParentScrollViewer_ViewChanged; + _hostScrollViewer = null; + } + } + + private void ParentScrollViewer_ViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) + { + var hostScrollViewer = (ScrollViewer)sender!; + + var associatedElementRect = AssociatedObject + .TransformToVisual(_hostScrollViewer) + .TransformBounds(new Rect(0, 0, AssociatedObject.ActualWidth, AssociatedObject.ActualHeight)); + + var hostScrollViewerRect = new Rect(0, 0, hostScrollViewer.ActualWidth, hostScrollViewer.ActualHeight); + + if (hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Top)) || + hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Top)) || + hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Bottom)) || + hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Bottom))) + { + IsInViewport = true; + + if (hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Top)) && + hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Top)) && + hostScrollViewerRect.Contains(new Point(associatedElementRect.Right, associatedElementRect.Bottom)) && + hostScrollViewerRect.Contains(new Point(associatedElementRect.Left, associatedElementRect.Bottom))) + { + IsFullyInViewport = true; + } + else + { + IsFullyInViewport = false; + } + } + else + { + IsInViewport = false; + } + } + + private void Init() + { + _hostScrollViewer = AssociatedObject.FindAscendant(); + if (_hostScrollViewer == null) + { + throw new InvalidOperationException("This behavior can only be attached to an element which has a ScrollViewer as a parent."); + } + + _hostScrollViewer.ViewChanged += ParentScrollViewer_ViewChanged; + } + + private void AssociatedObject_Loaded(object sender, RoutedEventArgs e) + { + AssociatedObject.Loaded -= AssociatedObject_Loaded; + Init(); + } +} diff --git a/components/Behaviors/tests/Behaviors.Tests.projitems b/components/Behaviors/tests/Behaviors.Tests.projitems new file mode 100644 index 00000000..75ff7d8c --- /dev/null +++ b/components/Behaviors/tests/Behaviors.Tests.projitems @@ -0,0 +1,23 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + C5924901-9000-4A6D-936D-BD69944E8C8F + + + BehaviorsExperiment.Tests + + + + + ExampleBehaviorsTestPage.xaml + + + + + Designer + MSBuild:Compile + + + \ No newline at end of file diff --git a/components/Behaviors/tests/Behaviors.Tests.shproj b/components/Behaviors/tests/Behaviors.Tests.shproj new file mode 100644 index 00000000..1a5e6e46 --- /dev/null +++ b/components/Behaviors/tests/Behaviors.Tests.shproj @@ -0,0 +1,13 @@ + + + + C5924901-9000-4A6D-936D-BD69944E8C8F + 14.0 + + + + + + + + diff --git a/components/Behaviors/tests/ExampleBehaviorsTestClass.cs b/components/Behaviors/tests/ExampleBehaviorsTestClass.cs new file mode 100644 index 00000000..c22c3397 --- /dev/null +++ b/components/Behaviors/tests/ExampleBehaviorsTestClass.cs @@ -0,0 +1,41 @@ +// 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 CommunityToolkit.Tests; +using CommunityToolkit.Tests.Internal; +using CommunityToolkit.Tooling.TestGen; + +namespace BehaviorsExperiment.Tests; + +[TestClass] +public partial class ExampleBehaviorsTestClass : VisualUITestBase +{ + // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter. + // This lets us actually test a control as it would behave within an actual application. + // The page will already be loaded by the time your test is called. + [UIThreadTestMethod] + public void SimpleUIExamplePageTest(ExampleBehaviorsTestPage page) + { + // You can use the Toolkit Visual Tree helpers here to find the component by type or name: + /*var component = page.FindDescendant(); + + Assert.IsNotNull(component); + + var componentByName = page.FindDescendant("BehaviorsControl"); + + Assert.IsNotNull(componentByName);*/ + } + + // You can still do async work with a UIThreadTestMethod as well. + [UIThreadTestMethod] + public async Task SimpleAsyncUIExamplePageTest(ExampleBehaviorsTestPage page) + { + // This helper can be used to wait for a rendering pass to complete. + await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { }); + + /*var component = page.FindDescendant(); + + Assert.IsNotNull(component);*/ + } +} diff --git a/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml new file mode 100644 index 00000000..8afe86b9 --- /dev/null +++ b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml @@ -0,0 +1,11 @@ + + + + + diff --git a/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs new file mode 100644 index 00000000..b9bface8 --- /dev/null +++ b/components/Behaviors/tests/ExampleBehaviorsTestPage.xaml.cs @@ -0,0 +1,16 @@ +// 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. + +namespace BehaviorsExperiment.Tests; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ExampleBehaviorsTestPage : Page +{ + public ExampleBehaviorsTestPage() + { + this.InitializeComponent(); + } +} From d198865bf7874fcd888bb8f115bab791f5680667 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Fri, 14 Apr 2023 13:55:44 -0500 Subject: [PATCH 03/13] Initialize timer from constructor, removed nullability --- components/Behaviors/src/FocusBehavior.cs | 27 +++++++++-------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/components/Behaviors/src/FocusBehavior.cs b/components/Behaviors/src/FocusBehavior.cs index 1424b379..0e5fa18e 100644 --- a/components/Behaviors/src/FocusBehavior.cs +++ b/components/Behaviors/src/FocusBehavior.cs @@ -39,12 +39,18 @@ public sealed class FocusBehavior : BehaviorBase typeof(FocusBehavior), new PropertyMetadata(TimeSpan.FromMilliseconds(100))); - private DispatcherQueueTimer? _timer; + private DispatcherQueueTimer _timer; /// /// Initializes a new instance of the class. /// - public FocusBehavior() => Targets = new FocusTargetList(); + public FocusBehavior() + { + _timer = DispatcherQueue.GetForCurrentThread().CreateTimer(); + _timer.Tick += OnEngagementTimerTick; + + Targets = new FocusTargetList(); + } /// /// Gets or sets the ordered list of controls which should receive the focus when the associated object is loaded. @@ -150,18 +156,8 @@ private void ApplyFocus() // We have been able to set the focus on one control. // We start the timer to detect if we can focus another control with an higher priority. // This allows us to handle the case where the controls are not loaded in the order we expect. - if (_timer is null) - { -#if !WINAPPSDK - _timer = DispatcherQueue.GetForCurrentThread().CreateTimer(); -#else - _timer = DispatcherQueue.CreateTimer(); -#endif - - _timer.Interval = FocusEngagementTimeout; - _timer.Tick += OnEngagementTimerTick; - _timer.Start(); - } + _timer.Interval = FocusEngagementTimeout; + _timer.Start(); } } @@ -183,10 +179,9 @@ private void OnContainerContentChanging(ListViewBase sender, ContainerContentCha private void Stop(FocusTargetList? targets = null) { - if (_timer != null) + if (_timer.IsRunning) { _timer.Stop(); - _timer = null; } foreach (var target in targets ?? Targets) From b3faec05164eb3045d67262873f7f243305b9f51 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Mon, 17 Apr 2023 19:14:48 -0500 Subject: [PATCH 04/13] Ported samples and basic docs --- .../samples/AutoSelectBehaviorSample.xaml | 16 +++++ .../samples/AutoSelectBehaviorSample.xaml.cs | 16 +++++ components/Behaviors/samples/Behaviors.md | 40 +++++++++--- .../samples/FocusBehaviorButtonSample.xaml | 25 ++++++++ .../samples/FocusBehaviorButtonSample.xaml.cs | 18 ++++++ .../samples/FocusBehaviorListSample.xaml | 26 ++++++++ .../samples/FocusBehaviorListSample.xaml.cs | 16 +++++ .../samples/KeyDownTriggerBehaviorSample.xaml | 24 ++++++++ .../KeyDownTriggerBehaviorSample.xaml.cs | 26 ++++++++ .../samples/ViewportBehaviorSample.xaml | 61 +++++++++++++++++++ .../samples/ViewportBehaviorSample.xaml.cs | 17 ++++++ components/Behaviors/src/FocusBehavior.cs | 2 - 12 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 components/Behaviors/samples/AutoSelectBehaviorSample.xaml create mode 100644 components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs create mode 100644 components/Behaviors/samples/FocusBehaviorButtonSample.xaml create mode 100644 components/Behaviors/samples/FocusBehaviorButtonSample.xaml.cs create mode 100644 components/Behaviors/samples/FocusBehaviorListSample.xaml create mode 100644 components/Behaviors/samples/FocusBehaviorListSample.xaml.cs create mode 100644 components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml create mode 100644 components/Behaviors/samples/KeyDownTriggerBehaviorSample.xaml.cs create mode 100644 components/Behaviors/samples/ViewportBehaviorSample.xaml create mode 100644 components/Behaviors/samples/ViewportBehaviorSample.xaml.cs diff --git a/components/Behaviors/samples/AutoSelectBehaviorSample.xaml b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml new file mode 100644 index 00000000..0bdc4a4e --- /dev/null +++ b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs new file mode 100644 index 00000000..8fad7705 --- /dev/null +++ b/components/Behaviors/samples/AutoSelectBehaviorSample.xaml.cs @@ -0,0 +1,16 @@ +// 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 CommunityToolkit.WinUI.Behaviors; + +namespace BehaviorsExperiment.Samples; + +[ToolkitSample(id: nameof(AutoSelectBehaviorSample), nameof(AutoSelectBehavior), description: $"A sample for showing how to use the AutoSelectBehavior.")] +public sealed partial class AutoSelectBehaviorSample : Page +{ + public AutoSelectBehaviorSample() + { + this.InitializeComponent(); + } +} diff --git a/components/Behaviors/samples/Behaviors.md b/components/Behaviors/samples/Behaviors.md index 1345df4f..49e61584 100644 --- a/components/Behaviors/samples/Behaviors.md +++ b/components/Behaviors/samples/Behaviors.md @@ -13,13 +13,33 @@ issue-id: 0 # Behaviors -Xaml Behaviors are an easy way to add common and reusable interactivity to your app with minimal code. - -TODO: -- Simple overview of behaviors -- Link to official Xaml Behavior docs -- Document / create samples for: - - KeyDownTriggerBehavior - - AutoSelectBehavior - - ViewPortBehavior - - FocusBehavior +A behavior is a class that attaches to a XAML control and invokes an Action when triggered. + +See also [XamlBehaviors Wiki](https://github.com/Microsoft/XamlBehaviors/wiki) + +## KeyDownTriggerBehavior + +A behavior that listens to a key press event on the associated UIElement and triggers the set of actions. + +> [!Sample KeyDownTriggerBehaviorSample] + +## AutoSelectBehavior + +The AutoSelectBehavior automatically selects the entire content of its associated TextBox when it is loaded. + +> [!Sample AutoSelectBehaviorSample] + +## ViewportBehavior +This behavior allows you to listen an element enter or exit the ScrollViewer viewport. + +> [!Sample ViewportBehaviorSample] + +## FocusBehavior + +Of the given targets, this behavior sets the focus on the first control which accepts it. + +A control only receives focus if it is enabled and loaded into the visual tree: +> [!Sample FocusBehaviorButtonSample] + +Empty lists do not receive focus: +> [!Sample FocusBehaviorListSample] diff --git a/components/Behaviors/samples/FocusBehaviorButtonSample.xaml b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml new file mode 100644 index 00000000..2c64f32c --- /dev/null +++ b/components/Behaviors/samples/FocusBehaviorButtonSample.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + +