• 分享一个分页控件的实现思路


    虽然分页控件满天飞,因为实在没找到WinForm程序合用的,所以就造了一回轮子。一开始认为这个事情比较简单,没有思考太多就开工了。事实上也没花多少时间就写好了第一版,想要有的功能也都实现了,以为万事大吉。。。。。。控件的样子长这样:

    软件开发法则之一:如果一件事情特别顺利,那么一定会有一些坑在等着你!坑的大小和顺利程度成正比。

    果不其然,在前几天的业务模块重构时就掉分页的坑里面了,切换每页行数后总是加载两次数据。问题的原因也很简单,加载数据的事件被触发了两次。靠,看来这里业务逻辑有大问题啊!再看别的地方逻辑,也有问题!!!刚好遇到周末,于是,就开始一通全面梳理。怎么梳理呢?还是从需求出发。

    需求一:可以设置每页显示行数

    修改了每页显示行数后,需要反馈到ViewModel,好根据新的显示行数重新加载数据。等一下!似乎有的时候也不需要刷新数据吧?譬如当前每页显示20行,但总数只有10行,这个时候切换成每页100行,它还是只能显示10行啊。这个时候就不需要重新加载数据,能省就省啊。这个时候不去刷新数据,不但提高效率,体验也更好。

    需求二:可以切换页码,首页|上一页|下一页|末页|到[x]页

    切换页码后,需要反馈到ViewModel,好根据新的页码重新加载数据。这个直来直去的最简单了!嗯,当前页是首页的时候,首页|上一页 这两个按钮应该屏蔽掉,同样,当前页是末页时,下一页|末页 两个按钮也应该屏蔽掉。如果只有一页,那么这5个按钮都不应该可用。

    分页的基本需求也就这两个了,但我还需要一些特殊的需求。这些需求看上去挺简单的,譬如:

    1、新增一个对象后,将对象放到列表的最后,并且自动选中它。

    2、删除一个选定对象后,将对象从列表中移除。如果对象不是列表中最后一个对象,自动选中下一个对象,否则自动选中上一个对象(如果对象是当前页的唯一对象,则意味着上一个对象位于上一页,需要自动跳到上一页)。

    3、切换每页显示行数后还是选中当前对象,这就需要重新计算当前页。。。。。。好吧,这里就是大坑之所在了。到底是否需要重新加载数据呢?似乎逻辑相当复杂啊。。。。。。梳理了半天,总结出一句话:切换了页码或当前页实际显示行数变化后需要重新加载数据!

    业务逻辑的梳理到这里就完成了,接下去就是写代码实现的事情了。那么,对以上业务逻辑,需要如何设计呢?

    1、需要定义2个自定义事件、事件参数和对应的委托,用于通知使用者相应参数的变化和重新加载列表数据。

     1  /// <summary>
     2  /// 焦点行改变事件参数
     3  /// </summary>
     4  public class RowHandleEventArgs : EventArgs
     5  {
     6      /// <summary>
     7      /// Row handle
     8      /// </summary>
     9      public int rowHandle { get; }
    10 
    11      /// <summary>
    12      /// 构造函数
    13      /// </summary>
    14      /// <param name="handel">RowsPerPage</param>
    15      public RowHandleEventArgs(int handel)
    16      {
    17          rowHandle = handel;
    18      }
    19  }
    20 
    21  /// <summary>
    22  /// 页面重载事件参数
    23  /// </summary>
    24  public class PageReloadEventArgs : EventArgs
    25  {
    26      /// <summary>
    27      /// Row handle
    28      /// </summary>
    29      public int handle { get; }
    30 
    31      /// <summary>
    32      /// Current page
    33      /// </summary>
    34      public int page { get; }
    35 
    36      /// <summary>
    37      /// Page size
    38      /// </summary>
    39      public int size { get; }
    40 
    41      /// <summary>
    42      /// 构造函数
    43      /// </summary>
    44      /// <param name="handle">Row handle</param>
    45      /// <param name="page">Current page</param>
    46      /// <param name="size">Page size</param>
    47      public PageReloadEventArgs(int handle, int page, int size)
    48      {
    49          this.handle = handle;
    50          this.page = page;
    51          this.size = size;
    52      }
    53  }
    54  
    55 /// <summary>  
    56 /// 当前焦点行发生改变,通知修改焦点行
    57 /// </summary>  
    58 public event FocusedRowChangedHandle focusedRowChanged;
    59 
    60 /// <summary>
    61 /// 表示将处理当前焦点行发生改变事件的方法
    62 /// </summary>
    63 /// <param name="sender"></param>
    64 /// <param name="e"></param>
    65 public delegate void FocusedRowChangedHandle(object sender, RowHandleEventArgs e);
    66 
    67 /// <summary>  
    68 /// 当前页需要重新加载,通知重新加载列表数据
    69 /// </summary>  
    70 public event PageReloadHandle currentPageChanged;
    71 
    72 /// <summary>
    73 /// 表示将处理列表数据需重新加载事件的方法
    74 /// </summary>
    75 /// <param name="sender"></param>
    76 /// <param name="e"></param>
    77 public delegate void PageReloadHandle(object sender, PageReloadEventArgs e);

    2、需要定义5个属性,用来传递参数

     1  /// <summary>
     2  /// 每页行数下拉列表选项
     3  /// </summary>
     4  public Collection<string> pageSizeItems
     5  {
     6      get => pageSizes;
     7      set
     8      {
     9          pageSizes = value;
    10          cbeRows.Properties.Items.AddRange(value);
    11          cbeRows.SelectedIndex = 0;
    12          size = int.Parse(pageSizes[0]);
    13      }
    14  }
    15 
    16  /// <summary>
    17  /// 总行数
    18  /// </summary>
    19  public int totalRows
    20  {
    21      set
    22      {
    23          rows = value;
    24 
    25          refresh();
    26      }
    27  }
    28 
    29  /// <summary>
    30  /// 当前页
    31  /// </summary>
    32  public int page => current + 1;
    33 
    34  /// <summary>
    35  /// 当前每页行数
    36  /// </summary>
    37  public int size { get; private set; }
    38 
    39  /// <summary>
    40  /// 当前选中行Handle
    41  /// </summary>
    42  public int focusedRowHandle
    43  {
    44      get => handle - size * current;
    45      set => handle = size * current + value;
    46  }

    3、需要2个Public方法,用于增加/删除列表对象后处理相应业务逻辑

     1  /// <summary>
     2  /// 增加列表成员
     3  /// </summary>
     4  /// <param name="count">增加数量,默认1个</param>
     5  public void addItems(int count = 1)
     6  {
     7      rows += count;
     8      handle = rows - 1;
     9 
    10      refresh();
    11  }
    12 
    13  /// <summary>
    14  /// 减少列表成员
    15  /// </summary>
    16  /// <param name="count">减少数量,默认1个</param>
    17  public void removeItems(int count = 1)
    18  {
    19      rows -= count;
    20      handle = rows - 1;
    21 
    22      refresh();
    23  }

    剩下的就是内部的逻辑处理函数了

      1 /// <summary>
      2 /// 构造方法
      3 /// </summary>
      4 public PageControl()
      5 {
      6     InitializeComponent();
      7 
      8     cbeRows.EditValueChanged += (sender, args) => pageRowsChanged();
      9     btnFirst.Click += (sender, args) => changePage(0);
     10     btnPrev.Click += (sender, args) => changePage(current - 1);
     11     btnNext.Click += (sender, args) => changePage(current + 1);
     12     btnLast.Click += (sender, args) => changePage(totalPages);
     13     btnJump.Click += (sender, args) =>  jumpClick();
     14     txtPage.KeyPress += (sender, args) => pageInputKeyPress(args);
     15     txtPage.Leave += (sender, args) => pageInputLeave();
     16 }
     17 
     18 /// <summary>
     19 /// 切换每页行数
     20 /// </summary>
     21 private void pageRowsChanged()
     22 {
     23     size = int.Parse(cbeRows.Text);
     24     refresh(true);
     25 }
     26 
     27 /// <summary>
     28 /// 切换当前页
     29 /// </summary>
     30 /// <param name="page">页码</param>
     31 private void changePage(int page)
     32 {
     33     handle = size * page;
     34     refresh();
     35 }
     36 
     37 /// <summary>
     38 /// 刷新控件
     39 /// </summary>
     40 /// <param name="reload">是否强制重新加载</param>
     41 private void refresh(bool reload = false)
     42 {
     43     var currentPage = current;
     44     if (handle > rows) handle = 0;
     45 
     46     totalPages = rows / size;
     47     labRows.Text = $@" 行/页 | 共 {rows} 行 | 分 {totalPages +1} 页";
     48     labRows.Refresh();
     49 
     50     current = handle / size;
     51     btnFirst.Enabled = current > 0;
     52     btnPrev.Enabled = current > 0;
     53     btnNext.Enabled = current < totalPages - 1;
     54     btnLast.Enabled = current < totalPages - 1;
     55     btnJump.Enabled = totalPages > 1;
     56 
     57     var width = (int) Math.Log10(current + 1)*7 + 18;
     58     btnJump.Width = width;
     59     btnJump.Text = page.ToString();
     60     labRows.Focus();
     61 
     62     if (!reload && current == currentPage)
     63     {
     64         var eventArgs = new RowHandleEventArgs(focusedRowHandle);
     65         focusedRowChanged?.Invoke(this, eventArgs);
     66     }
     67     else
     68     {
     69         var eventArgs = new PageReloadEventArgs(focusedRowHandle, page, size);
     70         currentPageChanged?.Invoke(this, eventArgs);
     71     }
     72 }
     73 
     74 /// <summary>
     75 /// 跳转到指定页
     76 /// </summary>
     77 private void jumpClick()
     78 {
     79     txtPage.Visible = true;
     80     txtPage.Focus();
     81 }
     82 
     83 /// <summary>
     84 /// 焦点离开输入框
     85 /// </summary>
     86 private void pageInputLeave()
     87 {
     88     txtPage.EditValue = null;
     89     txtPage.Visible = false;
     90 }
     91 
     92 /// <summary>
     93 /// 输入页码
     94 /// </summary>
     95 /// <param name="e"></param>
     96 private void pageInputKeyPress(KeyPressEventArgs e)
     97 {
     98     if (e.KeyChar == 27)
     99     {
    100         txtPage.EditValue = null;
    101         txtPage.Visible = false;
    102         return;
    103     }
    104 
    105     if (e.KeyChar != 13) return;
    106 
    107     if (string.IsNullOrEmpty(txtPage.Text)) return;
    108 
    109     var val = int.Parse(txtPage.Text);
    110     if (val < 1 || val > totalPages || val == page)
    111     {
    112         txtPage.EditValue = null;
    113         return;
    114     }
    115 
    116     txtPage.Visible = false;
    117     changePage(val - 1);
    118 }

    完整代码见:https://github.com/xuanbg/Utility/tree/2.0/BaseForm/Controls

    经过重构后,分页控件对外仅暴露5个属性和2个方法。使用者只需要在参数变化后给相应属性赋值即可,每页行数的调整、加载列表数据和列表的FocusedRowHandle都通过订阅事件完成。代码示例如下:

    1 tab.currentPageChanged += (sender, args) => call("loadData", new object[] {args.handle});
    2 tab.focusedRowChanged += (sender, args) => grid.FocusedRowHandle = args.rowHandle;

    ————————————————默默无语的分割线——————————————————

    在这篇随笔发布后,又改了一点东西。把每页显示行数这个参数改成了通过事件参数传递,减少了一个属性。特别补充说明一下,FocusedRowHandle这个属性其实非常重要,有这个属性,在刷新或者改变每页显示行数后,焦点行就可以保持在原先选中行上面,这样界面就不会抖动。

    现在总结起来,一个分页控件只需要公开:2个方法、2个事件、5个属性。无论是做成什么样子,用什么语言,都是如此。

    如果这篇文字对看官有点用处的话,请帮忙点下推荐,谢谢!

  • 相关阅读:
    4.文本编辑器vi的简单实用与指针介绍
    3.理解make命令——编译源文件安装
    2.换一种方式理解linux命令行
    1.linux环境搭建
    Tomcat 何时解压war包
    正则表达式8---再谈小括号
    利用vue-resource模拟百度下拉列表
    那些年iframe的坑(一)
    $nextTick()的理解
    一个超简单的vue商品计算总金额
  • 原文地址:https://www.cnblogs.com/xuanbg/p/6197017.html
Copyright © 2020-2023  润新知