Поведение взаимодействия не вызывается с привязкой ListBox ItemSource

Это мой первый набег в WPF, и я стараюсь внимательно следить за MVVM, чтобы все было правильно. Контекст здесь заключается в том, что у меня есть представление, которое должно отображать различные наборы сообщений, все из которых хранятся в ObservableCollection<T> .

Это код моего представления (это UserControl, который размещен в другом представлении, поэтому я могу перемещаться между разными представлениями во время выполнения)

Пространство имен "i" - это xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

<ListBox ItemsSource="{Binding Messages}">
    <i:Interaction.Behaviors>
        <behaviours:ScrollOnNewItemBehaviour />
    </i:Interaction.Behaviors>
    <ListBox.ItemTemplate>
        <DataTemplate DataType="entities:DisplayedUserMessage">
            <!-- Removed for brevity -->
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Вот код для поведения (собранный из других вопросов SO, которые я просматривал, чтобы разобраться с концепцией):

public sealed class ScrollOnNewItemBehaviour : Behavior<ListBox>
{
    protected override void OnAttached()
    {
        AssociatedObject.Loaded += OnLoaded;
        AssociatedObject.Unloaded += OnUnLoaded;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.Loaded -= OnLoaded;
        AssociatedObject.Unloaded -= OnUnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged += OnCollectionChanged;
    }

    private void OnUnLoaded(object sender, RoutedEventArgs e)
    {
        var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
        if (incc == null) return;

        incc.CollectionChanged -= OnCollectionChanged;
    }

    private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (e.Action == NotifyCollectionChangedAction.Add)
        {
            var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
            var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);

            // Only scroll when we're scrolled to the bottom of the listbox
            if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
            {
                scrollViewer.ScrollToBottom();
            }
        }
    }
}

Итак, вот где возникает моя конкретная проблема - привязка работает просто отлично. Когда я изменяю _selectedChannel (я удалил нерелевантный код из ViewModel ниже), представление обновляется новыми сообщениями ( _messages - это словарь, который содержит различные экземпляры ObservableCollection), а когда я добавляю новые сообщения к ним, обновляется и пользовательский интерфейс.

Проблема в том, что ни в коем случае не срабатывает поведение, зарегистрированное в ListBox, что является проблемой, так как я полагаюсь на это, чтобы держать прокрутку. Мое лучшее предположение состояло в том, что, возможно, он не поддерживает связанный ItemSource, а тот факт, что ItemSource изначально имеет значение null (словарь будет заполняться асинхронно, поэтому по умолчанию не задан) означает, что он не был зарегистрирован должным образом / должен перерегистрируется каждый раз при обновлении привязки?

public MessagesViewModel : ViewModelBase
{
    private ObservableCollection<DisplayedUserMessage> _displayedMessages;
    private Channel _selectedChannel;

    public IList<DisplayedUserMessage> Messages
    {
        get
        {
            return _displayedMessages;
        }
        set
        {
            if (_displayedMessages == value)
            {
                return;
            }
            _displayedMessages = value;
            NotifyPropertyChanged();
        }
    }

    public Channel SelectedChannel
    {
        get
        {
            return _selectedChannel;
        }
        set
        {
            if (_selectedChannel == value)
            {
                return;
            }
            _selectedChannel = value;
            Messages = _messages[_selectedChannel.Id];
            NotifyPropertyChanged();
        }
    }
}

Поведение работает, если оно выполняется (я убедился, что оно не работает с точками останова), поэтому, если у кого-то есть идея относительно того, что мне следует изменить, чтобы это работало с изменением ItemSources, пожалуйста, дайте мне знать!

Всего 1 ответ


Вы можете подписаться на событие PropertyChanged AssociatedObject.DataContext и подождать, пока свойство Messages изменится на допустимое значение.

Или вы инициализируете Messages с пустой коллекцией и, как только элементы создаются, добавляете их в Messages (это вызывает CollectionChanged для каждого добавленного элемента).

Третье решение - подписаться на событие ItemsControl.ItemContainerGenerator.ItemsChanged . Он вызывается всякий раз, когда изменяется свойство ItemsControl.Items или коллекция.

Вы можете использовать это событие вместо того, чтобы полагаться на источник привязки ItemsSource для реализации INotifyCollectionChanged . Вам также не нужно требовать DataContext для реализации INotifyPropertyChanged .
Это сделает ваше поведение более универсальным и пригодным для повторного использования, поскольку теперь оно может работать с любым ItemsControl .
Это означает, что прослушивание ItemsControl.ItemContainerGenerator.ItemsChanged равно INotifyCollectionChanged.CollectionChanged :

private void OnLoaded(object sender, RoutedEventArgs e)
{
    var itemsControl = AssociatedObject as ItemsControl;
    if (itemsControl == null) return;

    itemsControl.ItemContainerGenerator.ItemsChanged += OnCollectionChanged;
}

private void OnCollectionChanged(object sender, ItemsChangedEventArgs e)
{
  if (e.Action == NotifyCollectionChangedAction.Add)
  {
    var border = (Border)VisualTreeHelper.GetChild(AssociatedObject, 0);
    var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);

    // Only scroll when we're scrolled to the bottom of the listbox
    if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
    {
      scrollViewer.ScrollToBottom();
    }
  }
}

Есть идеи?

10000