Skip to content

Commit a920345

Browse files
Merge pull request #4099 from lyf6lyf/focusBehavior
Improve FocusBehavior: support list view lazy render. support control x:load
2 parents 747497a + ffed1a6 commit a920345

File tree

3 files changed

+81
-5
lines changed

3 files changed

+81
-5
lines changed

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/FocusBehavior/FocusBehaviorPage.xaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<interactivity:Interaction.Behaviors>
1111
<behaviors:FocusBehavior>
1212
<behaviors:FocusTarget Control="{x:Bind disabledItem}" />
13+
<behaviors:FocusTarget Control="{x:Bind xLoadItem}" />
1314
<behaviors:FocusTarget Control="{x:Bind emptyList}" />
1415
<behaviors:FocusTarget Control="{x:Bind enabledItem}" />
1516
</behaviors:FocusBehavior>
@@ -27,5 +28,13 @@
2728
</ListView>
2829
<Button x:Name="enabledItem"
2930
Content="I can get the focus" />
31+
<StackPanel Orientation="Horizontal">
32+
<ToggleSwitch x:Name="LoadControl"
33+
OffContent="Load the item"
34+
OnContent="Unload the item" />
35+
<Button x:Name="xLoadItem"
36+
x:Load="{x:Bind LoadControl.IsOn, Mode=OneWay}"
37+
Content="I can get the focus when loaded" />
38+
</StackPanel>
3039
</StackPanel>
3140
</Page>

Microsoft.Toolkit.Uwp.SampleApp/SamplePages/FocusBehavior/FocusBehaviorXaml.bind

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<interactivity:Interaction.Behaviors>
1010
<behaviors:FocusBehavior>
1111
<behaviors:FocusTarget Control="{x:Bind disabledItem}" />
12+
<behaviors:FocusTarget Control="{x:Bind xLoadItem}" />
1213
<behaviors:FocusTarget Control="{x:Bind emptyList}" />
1314
<behaviors:FocusTarget Control="{x:Bind enabledItem}" />
1415
</behaviors:FocusBehavior>
@@ -27,5 +28,13 @@
2728
</ListView>
2829
<Button x:Name="enabledItem"
2930
Content="I can get the focus" />
31+
<StackPanel Orientation="Horizontal">
32+
<ToggleSwitch x:Name="LoadControl"
33+
OffContent="Load the item"
34+
OnContent="Unload the item" />
35+
<Button x:Name="xLoadItem"
36+
x:Load="{x:Bind LoadControl.IsOn, Mode=OneWay}"
37+
Content="I can get the focus when loaded" />
38+
</StackPanel>
3039
</StackPanel>
3140
</Page>

Microsoft.Toolkit.Uwp.UI.Behaviors/Focus/FocusBehavior.cs

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Behaviors
1818
/// The focus will be set following the <see cref="Targets"/> order. The first control being ready
1919
/// and accepting the focus will receive it.
2020
/// The focus can be set to another control with a higher priority if it loads before <see cref="FocusEngagementTimeout"/>.
21+
/// The focus can be set to another control if some controls will be loaded/unloaded later.
2122
/// </summary>
2223
[ContentProperty(Name = nameof(Targets))]
2324
public sealed class FocusBehavior : BehaviorBase<UIElement>
@@ -67,10 +68,27 @@ public TimeSpan FocusEngagementTimeout
6768
}
6869

6970
/// <inheritdoc/>
70-
protected override void OnAssociatedObjectLoaded() => ApplyFocus();
71+
protected override void OnAssociatedObjectLoaded()
72+
{
73+
foreach (var target in Targets)
74+
{
75+
target.ControlChanged += OnTargetControlChanged;
76+
}
77+
78+
ApplyFocus();
79+
}
7180

7281
/// <inheritdoc/>
73-
protected override void OnDetaching() => Stop();
82+
protected override bool Uninitialize()
83+
{
84+
foreach (var target in Targets)
85+
{
86+
target.ControlChanged -= OnTargetControlChanged;
87+
}
88+
89+
Stop();
90+
return true;
91+
}
7492

7593
private static void OnTargetsPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
7694
{
@@ -92,24 +110,39 @@ private void ApplyFocus()
92110
}
93111

94112
var focusedControlIndex = -1;
113+
var hasListViewBaseControl = false;
95114
for (var i = 0; i < Targets.Count; i++)
96115
{
97116
var control = Targets[i].Control;
117+
if (control is null)
118+
{
119+
continue;
120+
}
121+
98122
if (control.IsLoaded)
99123
{
100124
if (control.Focus(FocusState.Programmatic))
101125
{
102126
focusedControlIndex = i;
103127
break;
104128
}
129+
130+
if (control is ListViewBase listViewBase)
131+
{
132+
// The list may not have any item yet, we wait until the first item is rendered.
133+
listViewBase.ContainerContentChanging -= OnContainerContentChanging;
134+
listViewBase.ContainerContentChanging += OnContainerContentChanging;
135+
hasListViewBaseControl = true;
136+
}
105137
}
106138
else
107139
{
140+
control.Loaded -= OnControlLoaded;
108141
control.Loaded += OnControlLoaded;
109142
}
110143
}
111144

112-
if (focusedControlIndex == 0 || Targets.All(t => t.Control?.IsLoaded == true))
145+
if (focusedControlIndex == 0 || (!hasListViewBaseControl && Targets.All(t => t.Control?.IsLoaded == true)))
113146
{
114147
// The first control has received the focus or all the control are loaded and none can take the focus: we stop.
115148
Stop();
@@ -137,6 +170,14 @@ private void OnEngagementTimerTick(object sender, object e)
137170

138171
private void OnControlLoaded(object sender, RoutedEventArgs e) => ApplyFocus();
139172

173+
private void OnTargetControlChanged(object sender, EventArgs e) => ApplyFocus();
174+
175+
private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
176+
{
177+
sender.ContainerContentChanging -= OnContainerContentChanging;
178+
ApplyFocus();
179+
}
180+
140181
private void Stop(FocusTargetList targets = null)
141182
{
142183
if (_timer != null)
@@ -153,6 +194,11 @@ private void Stop(FocusTargetList targets = null)
153194
}
154195

155196
target.Control.Loaded -= OnControlLoaded;
197+
198+
if (target.Control is ListViewBase listViewBase)
199+
{
200+
listViewBase.ContainerContentChanging -= OnContainerContentChanging;
201+
}
156202
}
157203
}
158204
}
@@ -167,7 +213,7 @@ public sealed class FocusTargetList : List<FocusTarget>
167213
/// <summary>
168214
/// A target for the <see cref="FocusBehavior"/>.
169215
/// </summary>
170-
public sealed partial class FocusTarget : DependencyObject
216+
public sealed class FocusTarget : DependencyObject
171217
{
172218
/// <summary>
173219
/// The DP to store the <see cref="Control"/> property value.
@@ -176,7 +222,13 @@ public sealed partial class FocusTarget : DependencyObject
176222
nameof(Control),
177223
typeof(Control),
178224
typeof(FocusTarget),
179-
new PropertyMetadata(null));
225+
new PropertyMetadata(null, OnControlChanged));
226+
227+
/// <summary>
228+
/// Raised when <see cref="Control"/> property changed.
229+
/// It can change if we use x:Load on the control.
230+
/// </summary>
231+
public event EventHandler ControlChanged;
180232

181233
/// <summary>
182234
/// Gets or sets the control that will receive the focus.
@@ -186,6 +238,12 @@ public Control Control
186238
get => (Control)GetValue(ControlProperty);
187239
set => SetValue(ControlProperty, value);
188240
}
241+
242+
private static void OnControlChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
243+
{
244+
var target = (FocusTarget)d;
245+
target.ControlChanged?.Invoke(target, EventArgs.Empty);
246+
}
189247
}
190248
#pragma warning restore SA1402 // File may only contain a single type
191249
}

0 commit comments

Comments
 (0)