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

虽然分页控件满天飞,因为实在没找到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个属性。无论是做成什么样子,用什么语言,都是如此。

 

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

posted @ 2016-12-19 12:27  xuanbg  阅读(2965)  评论(5编辑  收藏  举报