在MVVM架构下实现将基于ListViewBase控件的显示项滚动到可视范围内(Windows 8.1)
当我们在开发Windows Store应用的时候,一个常见的场景是,你需要在两个页面之间相互跳转,一个是项目列表页面,一个是详细信息页面。当你点击项目列表页面的某一个项目时,就会跳转到相应的详细信息页面,然后通过回退按钮,你就可以回到原来的项目列表页面。当项目列表的内容很多的时候,回退按钮就会把你带到第一个项目对应的视图上,这当然不是我们希望的,我们希望当页面会退的时候,项目列表页面还是能够显示刚才点击的那个项目。
这个问题很容易解决,Windows Store应用提供了两个API来实现这个功能:ScrollIntoView和MakeVisible。当基于LivstViewBase的控件,比如ListView, GridView单独使用的时候,那么就使用ScrollIntoView这个函数。如果基于ListViewBase的控件作为一个视图放在SemanticZoom控件中的时候,那么就使用MakeVisible函数。但是,这两个函数不是在什么时候调用都能够生效的,必须要等ListViewBase控件已经完成了布局以后才能够调用生效。在这里,比较保险的做法是在Page.Loaded事件处理函数中调用ScrollIntoView;在ListviewBase.Loaded事件处理函数中调用MakeVisible。这样的话,我们就需要将这些调用放在xaml的后台代码中,对于一个遵循MVVM设计模式的程序,这是需要尽量避免的。所以,这里我决定实现一个Behavior来完成将显示项滚动到可视范围内这个功能。
Behavior是Windows 8.1新支持的功能,为了实现Behavior,你需要定义一个实现了IBehavior接口的类,这个接口要求我们实现一个属性和两个函数:
public interface IBehavior
{
DependencyObject AssociatedObject { get; }
void Attach(DependencyObject associatedObject);
void Detach();
}
AssociateObject属性很容易实现,它的值可以通过Attach函数得到,我们只需要实现标准的属性代码就可以了:
private DependencyObject _associatedObject;
public DependencyObject AssociatedObject
{
get
{
return _associatedObject;
}
}
为了实现Behavior的功能,这里我也定义了一个依赖属性LastFocusedItem,它用来设置最后得到焦点的那项,也就是我们需要滚动到显示范围内的那一项:
private static readonly DependencyProperty LastFocusedItemProperty = DependencyProperty.Register("LastFocusedItem", typeof(object), typeof(ScrollIntoViewBehavior), null);
public object LastFocusedItem
{
get
{
return (object)base.GetValue(LastFocusedItemProperty);
}
set
{
base.SetValue(LastFocusedItemProperty, (object)value);
}
}
Attach函数会在每次Behavior附着的那个对象加载的时候调用,在Attach函数中,您可以从输入参数中得到这个关联对象。这里由于我们将这个Behavior附着在ListViewBase控件上,我们可以将在这里添加ListViewBase.Loaded的事件处理函数:
public void Attach(DependencyObject associatedObj)
{
if (associatedObj != _associatedObject)
{
_associatedObject = associatedObj;
var lv = _associatedObject as ListViewBase;
if (lv != null)
{
lv.Loaded += lv_Loaded;
lv.Unloaded += lv_Unloaded;
}
}
}
在lv_Loaded事件处理函数中,我们首先检查该ListViewBase控件是否是放在SemanticZoom中使用的,如果是的,那么就调用MakeVisible来讲当前视图滚动到LastFocusedItem对应的位置。如果不是的话,那么我们再寻找到页面对象,为Page.Loaded添加时间处理函数:
void lv_Loaded(object sender, RoutedEventArgs e)
{
var sz = FindSemanticZoom();
if (sz != null)
{
if (_associatedObject != null)
{
ListViewBase lv = _associatedObject as ListViewBase;
SemanticZoomLocation szLocation = new SemanticZoomLocation() { Item = LastFocusedItem };
lv.MakeVisible(szLocation);
}
}
else
{
var page = FindPage();
if (page != null)
{
page.Loaded += page_Loaded;
}
}
}
在上面的代码里用到了两个辅助函数FindSemanticZoom和FindPage分别用来寻找SemanticZoom和Page对象,这两个函数都是通过VisualTreeHelper的GetParent函数来实现的:
private Page FindPage()
{
DependencyObject parent;
for (DependencyObject obj = _associatedObject; obj != null; obj = parent)
{
parent = VisualTreeHelper.GetParent(obj);
Page page = parent as Page;
if (page != null)
{
return page;
}
}
return null;
}
private SemanticZoom FindSemanticZoom()
{
DependencyObject parent;
for (DependencyObject obj = _associatedObject; obj != null; obj = parent)
{
parent = VisualTreeHelper.GetParent(obj);
SemanticZoom sz = parent as SemanticZoom;
if (sz != null)
{
return sz;
}
}
return null;
}
在page_Loaded事件处理函数中, 我们就能安全的调用ScrollIntoView来滚动到LastFocusedItem所在的视图了。
void page_Loaded(object sender, RoutedEventArgs e)
{
if (_associatedObject != null)
{
ListViewBase lv = _associatedObject as ListViewBase;
lv.ScrollIntoView(LastFocusedItem);
}
}
最后,我们看一下如何在XAML中使用这个Behavior:
<GridView SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<Interactivity:Interaction.Behaviors>
<utility:ScrollIntoViewBehavior LastFocusedItem="{Binding SelectedItem}"/>
</Interactivity:Interaction.Behaviors>
</GridView>
采用这种方法,无论ListViewBase控件是否在SematicZoom控件中,我们都可以滚动到正确的位置,而且也不会破坏MVVM的架构。由于只需要在XAML中添加Behavior,你完全可以帮这个工作交给UI设计师去完成,很好的实现了代码逻辑和UI界面的分离。这里我也将ScrollIntoViewBehavior的代码附上以供你参考。