Прокрутка WPF ListBox к выбранному элементу, установленному в коде в модели представления
У меня есть представление XAML со списком:
<control:ListBoxScroll ItemSource="{Binding Path=FooCollection}"
SelectedItem="{Binding SelectedFoo, Mode=TwoWay}"
ScrollSelectedItem="{Binding SelectedFoo}">
<!-- data templates, etc. -->
</control:ListBoxScroll>
Выбранный элемент привязан к свойству в моем представлении. Когда пользователь выбирает элемент в списке, меняет свойство SelectedFoo в модели представления. Когда я устанавливаю свойство SelectedFoo в моей модели просмотра, в поле списка выбирается правильный элемент.
Проблема в том, что если SelectedFoo, установленный в коде, в настоящее время не отображается, мне нужно дополнительно вызвать ScrollIntoView
в списке. Поскольку мой ListBox находится внутри представления, и моя логика находится внутри моей модели представления... Я не мог найти удобный способ сделать это. Поэтому я расширил ListBoxScroll:
class ListBoxScroll : ListBox
{
public static readonly DependencyProperty ScrollSelectedItemProperty = DependencyProperty.Register(
"ScrollSelectedItem",
typeof(object),
typeof(ListBoxScroll),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender,
new PropertyChangedCallback(onScrollSelectedChanged)));
public object ScrollSelectedItem
{
get { return (object)GetValue(ScrollSelectedItemProperty); }
set { SetValue(ScrollSelectedItemProperty, value); }
}
private static void onScrollSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var listbox = d as ListBoxScroll;
listbox.ScrollIntoView(e.NewValue);
}
}
Он в основном предоставляет новое свойство зависимостей ScrollSelectedItem
, которое я привязываю к свойству SelectedFoo
в моей модели представления. Затем я перехватываю свойство измененного обратного вызова зависимого свойства и прокручиваю вновь выбранный элемент в виде.
Кто-нибудь еще знает о более удобном способе вызова функций на пользовательских элементах управления в представлении XAML, которое поддерживается моделью просмотра? Это немного бегать, чтобы:
- создать зависимое свойство
- добавить обратный вызов свойства измененного обратного вызова
- вызов функции обработки внутри статического обратного вызова
Было бы неплохо поставить логику в методе ScrollSelectedItem { set {
, но структура зависимостей, похоже, прокралась и справилась с работой, фактически не называя ее.
Ответы
Ответ 1
После просмотра ответов возникла общая тема: внешние классы, которые прослушивают событие SelectionChanged в ListBox. Это заставило меня понять, что подход, основанный на зависимом имуществе, был чрезмерным, и я мог просто прослушать его подкласс:
class ListBoxScroll : ListBox
{
public ListBoxScroll() : base()
{
SelectionChanged += new SelectionChangedEventHandler(ListBoxScroll_SelectionChanged);
}
void ListBoxScroll_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ScrollIntoView(SelectedItem);
}
}
Я считаю, что это самое простое решение, которое делает то, что я хочу.
Почетное упоминание относится к adcool2007 для создания Behaviors. Вот несколько статей для интересующихся:
http://blogs.msdn.com/b/johngossman/archive/2008/05/07/the-attached-behavior-pattern.aspx
http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx
Я думаю, что для общих типов поведения, которые будут добавлены к нескольким различным пользовательским элементам управления (например, поведение по клику, поведение перетаскивания, поведение анимации и т.д.), тогда приложенное поведение имеет большой смысл. Причина, по которой я не хочу использовать их в этом конкретном случае, заключается в том, что реализация поведения (вызов ScrollIntoView
) не является общим действием, которое может произойти с любым элементом управления, отличным от ListBox.
Ответ 2
Вы пытались использовать Поведение... Вот ScrollInViewBehavior. Я использовал его для ListView и DataGrid..... Я думаю, что он должен работать для ListBox......
Вы должны добавить ссылку на System.Windows.Interactivity
для использования Behavior<T> class
Поведение
public class ScrollIntoViewForListBox : Behavior<ListBox>
{
/// <summary>
/// When Beahvior is attached
/// </summary>
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.SelectionChanged += AssociatedObject_SelectionChanged;
}
/// <summary>
/// On Selection Changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void AssociatedObject_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (sender is ListBox)
{
ListBox listBox = (sender as ListBox);
if (listBox .SelectedItem != null)
{
listBox.Dispatcher.BeginInvoke(
(Action) (() =>
{
listBox.UpdateLayout();
if (listBox.SelectedItem !=
null)
listBox.ScrollIntoView(
listBox.SelectedItem);
}));
}
}
}
/// <summary>
/// When behavior is detached
/// </summary>
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.SelectionChanged -=
AssociatedObject_SelectionChanged;
}
}
Использование
Добавьте псевдоним к XAML
как xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
затем в Control
<ListBox ItemsSource="{Binding Path=MyList}"
SelectedItem="{Binding Path=MyItem,
Mode=TwoWay}"
SelectionMode="Single">
<i:Interaction.Behaviors>
<Behaviors:ScrollIntoViewForListBox />
</i:Interaction.Behaviors>
</ListBox>
Теперь, когда свойство "MyItem" установлено в ViewModel
, список будет прокручиваться, когда изменения будут отбираться.
Ответ 3
Поскольку это строго проблема просмотра, нет причин, по которым у вас не может быть обработчик события в коде, стоящем за вашим представлением для этой цели. Прослушайте ListBox.SelectionChanged
и используйте это, чтобы прокручивать вновь выбранный элемент в виде.
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
((ListBox)sender).ScrollIntoView(e.AddedItems[0]);
}
Для этого также не требуется производная ListBox
. Просто используйте стандартный элемент управления, и когда значение ListBox.SelectedItem
изменится (как описано в исходном вопросе), указанный выше обработчик будет выполнен, и элемент будет прокручиваться в представлении.
<ListBox
ItemsSource="{Binding Path=FooCollection}"
SelectedItem="{Binding Path=SelectedFoo}"
SelectionChanged="ListBox_SelectionChanged"
/>
Другим подходом было бы написать прикрепленное свойство, которое прослушивает ICollectionView.CurrentChanged
, а затем вызывает ListBox.ScrollIntoView
для нового текущего элемента. Это более "многоразовый" подход, если вам нужна эта функция для нескольких списков. Здесь вы можете найти хороший пример: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/
Ответ 4
Я знаю, что это старый вопрос, но мой недавний поиск той же проблемы привел меня к этому. Я хотел использовать подход к поведению, но не хотел, чтобы зависимость от Blend SDK просто давала мне Behavior<T>
, поэтому здесь мое решение без него:
public static class ListBoxBehavior
{
public static bool GetScrollSelectedIntoView(ListBox listBox)
{
return (bool)listBox.GetValue(ScrollSelectedIntoViewProperty);
}
public static void SetScrollSelectedIntoView(ListBox listBox, bool value)
{
listBox.SetValue(ScrollSelectedIntoViewProperty, value);
}
public static readonly DependencyProperty ScrollSelectedIntoViewProperty =
DependencyProperty.RegisterAttached("ScrollSelectedIntoView", typeof (bool), typeof (ListBoxBehavior),
new UIPropertyMetadata(false, OnScrollSelectedIntoViewChanged));
private static void OnScrollSelectedIntoViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var selector = d as Selector;
if (selector == null) return;
if (e.NewValue is bool == false)
return;
if ((bool) e.NewValue)
{
selector.AddHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
}
else
{
selector.RemoveHandler(Selector.SelectionChangedEvent, new RoutedEventHandler(ListBoxSelectionChangedHandler));
}
}
private static void ListBoxSelectionChangedHandler(object sender, RoutedEventArgs e)
{
if (!(sender is ListBox)) return;
var listBox = (sender as ListBox);
if (listBox.SelectedItem != null)
{
listBox.Dispatcher.BeginInvoke(
(Action)(() =>
{
listBox.UpdateLayout();
if (listBox.SelectedItem !=null)
listBox.ScrollIntoView(listBox.SelectedItem);
}));
}
}
}
а затем использование просто
<ListBox ItemsSource="{Binding Path=MyList}"
SelectedItem="{Binding Path=MyItem, Mode=TwoWay}"
SelectionMode="Single"
behaviors:ListBoxBehavior.ScrollSelectedIntoView="True">
Ответ 5
Попробуйте следующее:
private void lstBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
lstBox.ScrollIntoView(lstBox.SelectedItem);
}
Ответ 6
Я использую это (на мой взгляд) ясное и легкое решение
listView.SelectionChanged += (s, e) =>
listView.ScrollIntoView(listView.SelectedItem);
где listView
- это имя управления listView
в xaml, SelectedItem
зависит от моего MVVM, а код вставлен в конструктор в файл xaml.cs.
Ответ 7
После связывания различных методов я нашел следующее наиболее простым и лучшим
lstbox.Items.MoveCurrentToLast();
lstbox.ScrollIntoView(lstbox.Items.CurrentItem);
Ответ 8
Я взял Ankesh ответ и сделал его не зависящим от blend sdk. Недостатком моего решения является то, что оно будет применяться ко всем спискам в вашем приложении. Но вверху не нужен специальный класс.
Когда ваше приложение инициализируется...
internal static void RegisterFrameworkExtensionEvents()
{
EventManager.RegisterClassHandler(typeof(ListBox), ListBox.SelectionChangedEvent, new RoutedEventHandler(ScrollToSelectedItem));
}
//avoid "async void" unless used in event handlers (or logical equivalent)
private static async void ScrollToSelectedItem(object sender, RoutedEventArgs e)
{
if (sender is ListBox)
{
var lb = sender as ListBox;
if (lb.SelectedItem != null)
{
await lb.Dispatcher.BeginInvoke((Action)delegate
{
lb.UpdateLayout();
if (lb.SelectedItem != null)
lb.ScrollIntoView(lb.SelectedItem);
});
}
}
}
Это приведет к тому, что все ваши списки будут выбраны (что мне нравится как поведение по умолчанию).