Rob*_*ler 56 wpf scroll listbox
我有一个WPF ListBox设置为水平滚动.ItemsSource绑定到我的ViewModel类中的ObservableCollection.每次添加新项目时,我都希望ListBox向右滚动,以便新项目可见.
ListBox在DataTemplate中定义,因此我无法在代码隐藏文件中按名称访问ListBox.
如何让ListBox始终滚动以显示最新添加的项目?
我想知道ListBox何时添加了一个新项目,但是我没有看到这样做的事件.
Avi*_* P. 65
您可以使用附加属性扩展ListBox的行为.在你的情况下,我将定义一个附加属性ScrollOnNewItem,当设置为true挂钩到INotifyCollectionChanged列表框项目源的事件时,并在检测到新项目时,将列表框滚动到它.
例:
class ListBoxBehavior
{
static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ListBoxBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var listBox = d as ListBox;
if (listBox == null) return;
bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
if (newValue == oldValue) return;
if (newValue)
{
listBox.Loaded += ListBox_Loaded;
listBox.Unloaded += ListBox_Unloaded;
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);
}
else
{
listBox.Loaded -= ListBox_Loaded;
listBox.Unloaded -= ListBox_Unloaded;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);
}
}
private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
Associations[listBox] = new Capture(listBox);
}
static void ListBox_Unloaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
listBox.Unloaded -= ListBox_Unloaded;
}
static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var incc = listBox.Items as INotifyCollectionChanged;
if (incc == null) return;
listBox.Loaded -= ListBox_Loaded;
Associations[listBox] = new Capture(listBox);
}
class Capture : IDisposable
{
private readonly ListBox listBox;
private readonly INotifyCollectionChanged incc;
public Capture(ListBox listBox)
{
this.listBox = listBox;
incc = listBox.ItemsSource as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += incc_CollectionChanged;
}
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
listBox.ScrollIntoView(e.NewItems[0]);
listBox.SelectedItem = e.NewItems[0];
}
}
public void Dispose()
{
if (incc != null)
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
Run Code Online (Sandbox Code Playgroud)
用法:
<ListBox ItemsSource="{Binding SourceCollection}"
lb:ListBoxBehavior.ScrollOnNewItem="true"/>
Run Code Online (Sandbox Code Playgroud)
UPDATE按安德烈在下面的意见建议,我加了钩子,以检测改变ItemsSource的ListBox.
den*_*zov 21
<ItemsControl ItemsSource="{Binding SourceCollection}">
<i:Interaction.Behaviors>
<Behaviors:ScrollOnNewItem/>
</i:Interaction.Behaviors>
</ItemsControl>
public class ScrollOnNewItem : Behavior<ItemsControl>
{
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)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement == null) return;
frameworkElement.BringIntoView();
}
}
Run Code Online (Sandbox Code Playgroud)
小智 19
我找到了一个非常灵活的方法,只需更新列表框scrollViewer并将位置设置到底部.例如,在其中一个ListBox事件中调用此函数,例如SelectionChanged.
private void UpdateScrollBar(ListBox listBox)
{
if (listBox != null)
{
var border = (Border)VisualTreeHelper.GetChild(listBox, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
Run Code Online (Sandbox Code Playgroud)
我使用这个解决方案:http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/.
即使将listbox的ItemsSource绑定到在非UI线程中操作的ObservableCollection,它也能工作.
当添加新项目时,此附加行为会自动将列表框滚动到底部。
<ListBox ItemsSource="{Binding LoggingStream}">
<i:Interaction.Behaviors>
<behaviors:ScrollOnNewItemBehavior
IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</ListBox>
Run Code Online (Sandbox Code Playgroud)
在您的 中ViewModel,您可以绑定到布尔值IfFollowTail { get; set; }来控制自动滚动是否处于活动状态。
该行为做了所有正确的事情:
IfFollowTail=false在 ViewModel 中设置,ListBox 将不再滚动到新项目的底部。IfFollowTail=true在 ViewModel 中设置,ListBox 就会立即滚动到底部,并继续这样做。public class ScrollOnNewItemBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty IsActiveScrollOnNewItemProperty = DependencyProperty.Register(
name: "IsActiveScrollOnNewItem",
propertyType: typeof(bool),
ownerType: typeof(ScrollOnNewItemBehavior),
typeMetadata: new PropertyMetadata(defaultValue: true, propertyChangedCallback:PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
// Intent: immediately scroll to the bottom if our dependency property changes.
ScrollOnNewItemBehavior behavior = dependencyObject as ScrollOnNewItemBehavior;
if (behavior == null)
{
return;
}
behavior.IsActiveScrollOnNewItemMirror = (bool)dependencyPropertyChangedEventArgs.NewValue;
if (behavior.IsActiveScrollOnNewItemMirror == false)
{
return;
}
ListboxScrollToBottom(behavior.ListBox);
}
public bool IsActiveScrollOnNewItem
{
get { return (bool)this.GetValue(IsActiveScrollOnNewItemProperty); }
set { this.SetValue(IsActiveScrollOnNewItemProperty, value); }
}
public bool IsActiveScrollOnNewItemMirror { get; set; } = true;
protected override void OnAttached()
{
this.AssociatedObject.Loaded += this.OnLoaded;
this.AssociatedObject.Unloaded += this.OnUnLoaded;
}
protected override void OnDetaching()
{
this.AssociatedObject.Loaded -= this.OnLoaded;
this.AssociatedObject.Unloaded -= this.OnUnLoaded;
}
private IDisposable rxScrollIntoView;
private void OnLoaded(object sender, RoutedEventArgs e)
{
var changed = this.AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (changed == null)
{
return;
}
// Intent: If we scroll into view on every single item added, it slows down to a crawl.
this.rxScrollIntoView = changed
.ToObservable()
.ObserveOn(new EventLoopScheduler(ts => new Thread(ts) { IsBackground = true}))
.Where(o => this.IsActiveScrollOnNewItemMirror == true)
.Where(o => o.NewItems?.Count > 0)
.Sample(TimeSpan.FromMilliseconds(180))
.Subscribe(o =>
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
ListboxScrollToBottom(this.ListBox);
}));
});
}
ListBox ListBox => this.AssociatedObject;
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
this.rxScrollIntoView?.Dispose();
}
/// <summary>
/// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox.
/// </summary>
private static void ListboxScrollToBottom(ListBox listBox)
{
if (VisualTreeHelper.GetChildrenCount(listBox) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(listBox, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
}
Run Code Online (Sandbox Code Playgroud)
最后,添加此扩展方法,以便我们可以利用所有 RX 优点:
public static class ListBoxEventToObservableExtensions
{
/// <summary>Converts CollectionChanged to an observable sequence.</summary>
public static IObservable<NotifyCollectionChangedEventArgs> ToObservable<T>(this T source)
where T : INotifyCollectionChanged
{
return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => (sender, e) => h(e),
h => source.CollectionChanged += h,
h => source.CollectionChanged -= h);
}
}
Run Code Online (Sandbox Code Playgroud)
您需要添加Reactive Extensions到您的项目中。我建议NuGet。
| 归档时间: |
|
| 查看次数: |
42584 次 |
| 最近记录: |