分享一个分页控件的实现思路
Posted xuanbg
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分享一个分页控件的实现思路相关的知识,希望对你有一定的参考价值。
虽然分页控件满天飞,因为实在没找到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个属性。无论是做成什么样子,用什么语言,都是如此。
如果这篇文字对看官有点用处的话,请帮忙点下推荐,谢谢!
以上是关于分享一个分页控件的实现思路的主要内容,如果未能解决你的问题,请参考以下文章
WPF 实现 DataGrid/ListView 分页控件(转)