Open kayle opened 7 years ago
Can you provide a sample?
Took a bit to find a more minimal repro, but here it is. Note that it seems to be related to ItemsStackPanel, after switching to StackPanel things worked as expected.
New Blank UWP Desktop app
MainPage.xaml
<StackPanel>
<Button Click="Button_Click" Content="Run" />
<ScrollViewer Name="Scroller" Height="300" >
<ItemsControl Name="Items" ItemTemplateSelector="{StaticResource Selector}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</StackPanel>
MainPage.xaml.cs
public class BindableFocusBehavior : Behavior<Control>
{
public static readonly DependencyProperty HasFocusProperty =
DependencyProperty.Register("HasFocus", typeof(bool), typeof(BindableFocusBehavior), new PropertyMetadata(default(bool), HasFocusUpdated));
private static void HasFocusUpdated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((BindableFocusBehavior)d).SetFocus();
}
protected override void OnDetaching()
{
base.OnDetaching();
}
public bool HasFocus
{
get { return (bool)GetValue(HasFocusProperty); }
set { SetValue(HasFocusProperty, value); }
}
private void SetFocus()
{
if (HasFocus)
{
AssociatedObject.Focus(FocusState.Keyboard);
}
}
}
public class FocusObject : INotifyPropertyChanged
{
public bool HasFocus { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasFocus)));
}
}
public class Selector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
return (DataTemplate)App.Current.Resources["Template1"];
}
}
public sealed partial class MainPage : Page
{
private readonly ObservableCollection<FocusObject> source = new ObservableCollection<FocusObject>();
public MainPage()
{
this.InitializeComponent();
Items.ItemsSource = source;
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 100; i++)
source.Add(new FocusObject());
while (true)
{
foreach (FocusObject o in Items.Items)
{
o.HasFocus = true;
o.RaisePropertyChanged();
o.HasFocus = false;
o.RaisePropertyChanged();
await System.Threading.Tasks.Task.Delay(10);
}
}
}
}
App.xaml
<Application.Resources>
<ResourceDictionary>
<local:Selector x:Key="Selector" />
<DataTemplate x:Key="Template1">
<TextBox Text="one" ScrollViewer.BringIntoViewOnFocusChange="True" >
<i:Interaction.Behaviors>
<local:BindableFocusBehavior HasFocus="{Binding HasFocus}" />
</i:Interaction.Behaviors>
</TextBox>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
It will hit a null reference when focusing items the second time through the the list.
Though I didn't try this yet, I can provide a theory for why this is happening: ItemsStackPanel
is virtualized, so you're trying to access AssociatedObject
before the behavior has actually attached (which is when that property gets set); StackPanel
however doesn't suffer from this condition, so it create an instance immediately and attach the behavior while doing so!
I have an ItemsControl which displays two different types of items (using ContentPresenter/Caliburn.Micro). When I add the first item that causes a scrollbar to appear, I'm seeing OnDetaching get called:
Each item I add after that causes OnAttached/OnDetaching to get called on the same BindableFocusBehavior instance, which leaves AssociatedObject null.
I'm not sure why the ItemsStackPanel is repeatedly loading/unloading the same UserControl, but is it possible that the events are being handled in the wrong order?
My workaround is currently just to store the AssociatedObject to a private field.