在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的代码附上以供你参考。

 

ScrollIntoViewBehavior .cs