看见很多人在求滚动条滑倒底部自动加载数据的解决方案,各种各样的方案很多,但令人满意的确没几个。在这里我分享一个我的自认为满意的解决方案。
public class ScrollViewerTrigger:TriggerBase<DependencyObject>
{
ScrollViewer ScrollView;
public static readonly DependencyProperty DirectionTypeProperty = DependencyProperty.Register("DirectionType", typeof(DirectionType), typeof(ScrollViewerTrigger), new PropertyMetadata(DirectionType.Bottom));
public DirectionType DirectionType
{
get
{
return (DirectionType)base.GetValue(ScrollViewerTrigger.DirectionTypeProperty);
}
set
{
base.SetValue(ScrollViewerTrigger.DirectionTypeProperty, value);
}
}
public event EventHandler ScrollTrigger;
public static readonly DependencyProperty VerticalOffsetProperty = DependencyProperty.Register("VerticalOffset", typeof(double), typeof(ScrollViewerTrigger), new PropertyMetadata(0.0, new PropertyChangedCallback(VerticalOffsetPropertyChanged)));
public double VerticalOffset
{
get
{
return (double)base.GetValue(ScrollViewerTrigger.VerticalOffsetProperty);
}
set
{
base.SetValue(ScrollViewerTrigger.VerticalOffsetProperty, value);
}
}
public static void VerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var behavior = d as ScrollViewerTrigger;
if (behavior != null)
behavior.OnVerticalOffsetChanged();
}
protected override void OnAttached()
{
base.OnAttached();
if (this.AssociatedObject != null && this.AssociatedObject is FrameworkElement)
{
(this.AssociatedObject as FrameworkElement).SizeChanged += control_SizeChanged;
}
}
void control_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.AssociatedObject == null || !(this.AssociatedObject is FrameworkElement))
return;
ScrollViewer Scroll = this.AssociatedObject.GetFirstDescendantOfType<ScrollViewer>();
if (Scroll != null)
{
AttachedScroll(Scroll);
(this.AssociatedObject as FrameworkElement).SizeChanged -= control_SizeChanged;
}
}
void AttachedScroll(ScrollViewer Scroll)
{
if (Scroll == null)
return;
ScrollView = Scroll;
Binding binding = new Binding();
binding.Source = Scroll;
binding.Path = new PropertyPath("VerticalOffset");
BindingOperations.SetBinding(this, ScrollViewerTrigger.VerticalOffsetProperty, binding);
}
void OnVerticalOffsetChanged()
{
ScrollViewer Scroll = ScrollView;
if (Scroll == null)
return;
switch (DirectionType)
{
case DirectionType.Top:
{
if (Scroll.ScrollableHeight < double.Epsilon || Scroll.VerticalOffset > double.Epsilon)
return;
}
break;
case DirectionType.Bottom:
{
if (Scroll.ScrollableHeight < double.Epsilon || Scroll.VerticalOffset + Scroll.ExtentHeight - Scroll.ScrollableHeight < Scroll.ScrollableHeight)
return;
}
break;
case DirectionType.Left:
{
if (Scroll.ScrollableWidth < double.Epsilon || Scroll.HorizontalOffset > double.Epsilon)
return;
}
break;
case DirectionType.Right:
{
if (Scroll.ScrollableWidth < double.Epsilon || Scroll.HorizontalOffset < Scroll.ScrollableWidth)
return;
}
break;
default: break;
}
if (ScrollTrigger != null)
{
ScrollTrigger(this.AssociatedObject, new EventArgs());
}
base.InvokeActions(null);
}
protected override void OnDetaching()
{
base.OnDetaching();
}
}
整个实现是Trigger的扩展,这样灵活性和通用性非常高,不管是什么控件。因为Trigger本身是附加元素,对于任何控件都可以附加上去,不要要改动原有代码就可以实现功能扩展。Trigger自带的Actions则有很强的灵活性,动作触发后可以执行多个Action.
void AttachedScroll(ScrollViewer Scroll)
{
if (Scroll == null)
return;
ScrollView = Scroll;
Binding binding = new Binding();
binding.Source = Scroll;
binding.Path = new PropertyPath("VerticalOffset");
BindingOperations.SetBinding(this, ScrollViewerTrigger.VerticalOffsetProperty, binding);
}
还记得我之前提到的VerticalOffset属性吗,我通过绑定将ScrollViewer中的 VerticalOffset属性与这个 Trigger中的VerticalOffset绑定在一起,这样ScrollViewer种的 VerticalOffset一旦有变化我这边可以实时指导。ScrollViewer中是没有 VerticalOffset变化通知的事件的,但通过绑定则可以解决这个问题。若做横向监控功能, binding.Path = new PropertyPath("VerticalOffset");换成 binding.Path = new PropertyPath("HorizontalOffset");就可以了。
<ListBox ItemsSource="{Binding List}">
<i:Interaction.Triggers>
<local:ScrollBarTrigger>
<i:InvokeCommandAction Command="{Binding MoreItemCommand}"/>
</local:ScrollBarTrigger>
</i:Interaction.Triggers>
</ListBox>
有些控件中没有ScrollViewer控件,但是有ScrollBar控件,我这里提供一个ScrollBarTrigger,原理与ScrollViewerTrigger稍有不同,哪里不同自己研究。另外ScrollBarTrigger比ScrollViewerTrigger更具通用性,因为ScrollViewer中包含ScrollBar。
public enum DirectionType { Top, Bottom, Left, Right }
public class ScrollBarTrigger : TriggerBase<DependencyObject>
{
ScrollBar ScrollView;
public static readonly DependencyProperty DirectionTypeProperty = DependencyProperty.Register("DirectionType", typeof(DirectionType), typeof(ScrollBarTrigger), new PropertyMetadata(DirectionType.Bottom));
public DirectionType DirectionType
{
get
{
return (DirectionType)base.GetValue(ScrollBarTrigger.DirectionTypeProperty);
}
set
{
base.SetValue(ScrollBarTrigger.DirectionTypeProperty, value);
}
}
public event EventHandler ScrollTrigger;
protected override void OnAttached()
{
base.OnAttached();
if (this.AssociatedObject != null && this.AssociatedObject is FrameworkElement)
{
(this.AssociatedObject as FrameworkElement).SizeChanged += control_SizeChanged;
}
}
protected override void OnDetaching()
{
base.OnDetaching();
if (ScrollView != null)
ScrollView.ValueChanged -= ScrollView_ValueChanged;
if (this.AssociatedObject != null && this.AssociatedObject is FrameworkElement)
{
(this.AssociatedObject as FrameworkElement).SizeChanged -= control_SizeChanged;
}
}
void control_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.AssociatedObject == null || !(this.AssociatedObject is FrameworkElement))
return;
ScrollBar Scroll = this.AssociatedObject.GetFirstDescendantOfType<ScrollBar>();
if (Scroll != null)
{
AttachedScroll(Scroll);
(this.AssociatedObject as FrameworkElement).SizeChanged -= control_SizeChanged;
}
}
void AttachedScroll(ScrollBar Scroll)
{
if (Scroll != null)
{
ScrollView = Scroll;
ScrollView.ValueChanged += ScrollView_ValueChanged;
}
}
void ScrollView_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
OnOffsetChanged();
}
void OnOffsetChanged()
{
if (ScrollView == null)
return;
ScrollBar Scroll = ScrollView;
switch (DirectionType)
{
case DirectionType.Top:
case DirectionType.Left:
{
if (Scroll.Maximum < double.Epsilon || Scroll.Value - Scroll.ViewportSize > 0)
return;
}
break;
case DirectionType.Bottom:
case DirectionType.Right:
{
if (Scroll.Maximum < double.Epsilon || Scroll.Value + Scroll.ViewportSize < Scroll.Maximum)
return;
}
break;
default: break;
}
if (ScrollTrigger != null)
{
ScrollTrigger(this.AssociatedObject,new EventArgs());
}
base.InvokeActions(null);
}
}
如果对Action,Trigger,Behavior有不了解的朋友可以看我之前的博客,会有详细解释。