microsoft / XamlBehaviors

This is the official home for UWP XAML Behaviors on GitHub.
MIT License
697 stars 112 forks source link

AssociatedObject is sometimes null within ItemsControl ItemTemplate #120

Open kayle opened 7 years ago

kayle commented 7 years ago

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:

>   BindableFocusBehavior.OnDetaching() Line 84 C#
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.Behavior.Detach() Unknown
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.BehaviorCollection.Detach()   Unknown
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.Interaction.FrameworkElement_Unloaded(object sender, Windows.UI.Xaml.RoutedEventArgs e)   Unknown

Each item I add after that causes OnAttached/OnDetaching to get called on the same BindableFocusBehavior instance, which leaves AssociatedObject null.

>   BindableFocusBehavior.OnAttached() Line 90  C#
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.Behavior.Attach(Windows.UI.Xaml.DependencyObject associatedObject)    Unknown
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.BehaviorCollection.Attach(Windows.UI.Xaml.DependencyObject associatedObject)  Unknown
    Microsoft.Xaml.Interactivity.dll!Microsoft.Xaml.Interactivity.Interaction.FrameworkElement_Loaded(object sender, Windows.UI.Xaml.RoutedEventArgs e) Unknown

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.

skendrot commented 7 years ago

Can you provide a sample?

kayle commented 7 years ago

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.

pedrolamas commented 7 years ago

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!