WPF DataGrid 如何将被选中行带到视野中

2022-12-06 19:09:12 浏览数 (1)

WPF DataGrid 如何将被选中行带到视野中

目录

前言

准备工作

方法一

方法二

总结

独立观察员 2021 年 12 月 11 日

前言

在 WPF 开发中,显示表格一般使用 DataGrid 控件,而且我们一般会依据用户的选中行的操作来执行一些逻辑,这种情况,选中了哪一行,用户是心知肚明的。而还有一种情况,我们可能在业务逻辑中,由程序自己选中了某一行,如果这一行当前不在用户界面的可视区(换句话说也就是滚动条没有滚到那个位置),那么我们如何将其带到用户的视野中呢?

准备工作

今天准备介绍两个方法。正所谓,工欲善其事必先利其器,所以在开始之前,我们先来构建一个可以模拟后台选中行的功能。

使用的还是之前用过的 DataGrid 的 Demo 程序(在《WPF DataGrid 通过自定义表头模拟首行固定》和《WPF 触屏事件后触发鼠标事件的问题及 DataGrid 误触问题》中用过),加了一个可以填写要选中的行号的文本框,以及一个执行选中操作的按钮:

下面来演示一下没有自动将选中行带到视野中的情况。我们先将数据添加到 10 条,然后缩小程序的窗口,这样有些数据就在滚动区外面了,也就是不在视野中。然后我们通过程序来选中行,可以看到选中功能是正常的,但是对于视野外的数据,用户看不到其是否选中,需要手动滚动来寻找,如下图(动图):

好,那接下来就介绍怎么解决吧。

方法一

这个方法是参考《【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF》文章中的将 TreeViewItem(树状列表项)带到视野中的方法,我稍微改造了一下,使其同时支持 DataGridRow 和 TreeViewItem,并且之后如果有其它受支持的类型也可以方便地扩展。BringIntoViewBehavior 类提供了一个 IsBroughtIntoViewWhenSelected 附加属性,给每个列表项的 Selected 事件指定了处理方法,处理方法中调用 BringIntoView () 方法,完整代码如下:

代码语言:javascript复制
using System.Windows;
using System.Windows.Controls;
/*
 * 源码已托管:https://gitee.com/dlgcy/WPFTemplateLib
 */
namespace WPFTemplateLib.WpfHelpers
{
    /// <summary>
    /// 功能:列表项被选中时带到视野中
    /// 参考:http://dlgcy.com/introduction-to-attached-behaviors-in-wpf/
    /// 说明:用于 DataGrid 时需要设置 EnableRowVirtualization="False"
    /// </summary>
    /// <example>
    /// <code>
    /// Setter Property="wpfHelpers:BringIntoViewBehavior.IsBroughtIntoViewWhenSelected" Value="True"/>
    /// </code>
    /// </example>
    public class BringIntoViewBehavior
    {
        #region IsBroughtIntoViewWhenSelected
        public static bool GetIsBroughtIntoViewWhenSelected(FrameworkElement item)
        {
            return (bool)item.GetValue(IsBroughtIntoViewWhenSelectedProperty);
        }
        public static void SetIsBroughtIntoViewWhenSelected(FrameworkElement item, bool value)
        {
            item.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
        }

        /// <summary>
        /// 是否在选中时带到视野中
        /// </summary>
        public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
            DependencyProperty.RegisterAttached(
                "IsBroughtIntoViewWhenSelected",
                typeof(bool),
                typeof(BringIntoViewBehavior),
                new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));

        static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
            FrameworkElement item = depObj as FrameworkElement;

            if (item == null)
                return;

            if (e.NewValue is bool == false)
                return;

            switch (depObj)
            {
                case DataGridRow row:
                {
                    if ((bool)e.NewValue)
                        row.Selected  = OnItemSelected;
                    else
                        row.Selected -= OnItemSelected;
                    break;
                }
                case TreeViewItem treeViewItem:
                {
                    if ((bool)e.NewValue)
                        treeViewItem.Selected  = OnItemSelected;
                    else
                        treeViewItem.Selected -= OnItemSelected;
                    break;
                }
                default:
                    break;
            }
        }

        static void OnItemSelected(object sender, RoutedEventArgs e)
        {
            // 忽略所有只是报告子孙的 Selected 被触发的祖先。
            if (!ReferenceEquals(sender, e.OriginalSource))
                return;

            if (e.OriginalSource is FrameworkElement item)
                item.BringIntoView();
        }

        #endregion
    }
}

此方法用于 DataGrid 时需要设置 EnableRowVirtualization="False"(默认为 true):

使用时只要在行样式中应用这个附加属性即可:

注意引入命名空间:

效果如下(动图):

方法二

如果开了行虚拟化(EnableRowVirtualization="True"),离可视区较远的行的 Selected 事件就不会被触发,以上方法就不行了。

类似于这个帖子的情况《WPF 开启行虚拟化的时候,行选择功能不正常,求解决方案》(https://bbs.csdn.net/topics/392666509):

所以如果因为数据量比较大必须开启行虚拟化时,可以使用下面的方法。

先给 DataGrid 命个名(如 x:Name="Dg" )方便后台使用,然后在 ViewModel 中添加一个选中项改变事件 SelectedItemChanged,并在选中项改变时调用(参数为选中行的索引):

代码语言:javascript复制
/// <summary>
/// 选中项改变事件
/// </summary>
public event Action<int> SelectedItemChanged;

private User _SelectedItem;
/// <summary>
/// 选中项
/// </summary>
public User SelectedItem
{
    get => _SelectedItem;
    set
    {
        SetProperty(ref _SelectedItem, value);

        SelectedItemChanged?.Invoke(Datas.IndexOf(_SelectedItem));
    }
}

接着在后台事件中注册事件处理方法,处理方法中调用了 DataGrid 的 ScrollIntoView 方法,代码如下:

代码语言:javascript复制
_vm.SelectedItemChanged  = OnSelectedItemChanged;

/// <summary>
/// 选中项改变事件执行方法
/// </summary>
/// <param name="index">选中行索引</param>
private void OnSelectedItemChanged(int index)
{
    Dg.ScrollIntoView(Dg.Items.GetItemAt(index));
}

效果和方法一的一样,就不再赘述了。

总结

关于将 DataGrid 选中行带到视野中的需求,本文介绍了两种方法。方法一提供了一个附加属性,可以方便地实现该需求,不过要求不能开启行虚拟化。方法二则是需要在 ViewModel 和页面后台编写代码,通过事件来触发相关操作,不过可以支持行虚拟化。大家可以依据实际情况选择使用,如果有更好的方法,欢迎交流。

源代码地址:https://gitee.com/dlgcy/DLGCY_WPFPractice/tree/Blog20211211

0 人点赞