虽然分页控件满天飞,因为实在没找到WinForm程序合用的,所以就造了一回轮子。一开始认为这个事情比较简单,没有思考太多就开工了。事实上也没花多少时间就写好了第一版,想要有的功能也都实现了,以为万事大吉。。。。。。控件的样子长这样:
软件开发法则之一:如果一件事情特别顺利,那么一定会有一些坑在等着你!坑的大小和顺利程度成正比。
果不其然,在前几天的业务模块重构时就掉分页的坑里面了,切换每页行数后总是加载两次数据。问题的原因也很简单,加载数据的事件被触发了两次。靠,看来这里业务逻辑有大问题啊!再看别的地方逻辑,也有问题!!!刚好遇到周末,于是,就开始一通全面梳理。怎么梳理呢?还是从需求出发。
需求一:可以设置每页显示行数
修改了每页显示行数后,需要反馈到ViewModel,好根据新的显示行数重新加载数据。等一下!似乎有的时候也不需要刷新数据吧?譬如当前每页显示20行,但总数只有10行,这个时候切换成每页100行,它还是只能显示10行啊。这个时候就不需要重新加载数据,能省就省啊。这个时候不去刷新数据,不但提高效率,体验也更好。
需求二:可以切换页码,首页|上一页|下一页|末页|到[x]页
切换页码后,需要反馈到ViewModel,好根据新的页码重新加载数据。这个直来直去的最简单了!嗯,当前页是首页的时候,首页|上一页 这两个按钮应该屏蔽掉,同样,当前页是末页时,下一页|末页 两个按钮也应该屏蔽掉。如果只有一页,那么这5个按钮都不应该可用。
分页的基本需求也就这两个了,但我还需要一些特殊的需求。这些需求看上去挺简单的,譬如:
1、新增一个对象后,将对象放到列表的最后,并且自动选中它。
2、删除一个选定对象后,将对象从列表中移除。如果对象不是列表中最后一个对象,自动选中下一个对象,否则自动选中上一个对象(如果对象是当前页的唯一对象,则意味着上一个对象位于上一页,需要自动跳到上一页)。
3、切换每页显示行数后还是选中当前对象,这就需要重新计算当前页。。。。。。好吧,这里就是大坑之所在了。到底是否需要重新加载数据呢?似乎逻辑相当复杂啊。。。。。。梳理了半天,总结出一句话:切换了页码或当前页实际显示行数变化后需要重新加载数据!
业务逻辑的梳理到这里就完成了,接下去就是写代码实现的事情了。那么,对以上业务逻辑,需要如何设计呢?
1、需要定义3个自定义事件和一个委托(因为需要通过事件传递参数),用于通知使用者相应参数的变化和重新加载列表数据
1 /// <summary> 2 /// 每页行数发生改变,通知修改每页行数 3 /// </summary> 4 public event EventHandler RowsPerPageChanged; 5 6 /// <summary> 7 /// 当前页发生改变,通知重新加载列表数据 8 /// </summary> 9 public event PageControlHandle CurrentPageChanged; 10 11 /// <summary> 12 /// 总行数发生改变,通知修改FocusedRowHandle 13 /// </summary> 14 public event PageControlHandle TotalRowsChanged; 15 16 /// <summary> 17 /// 表示将处理分页控件事件的方法 18 /// </summary> 19 /// <param name="sender"></param> 20 /// <param name="e"></param> 21 public delegate void PageControlHandle(object sender, PageControlEventArgs e);
2、需要定义5个属性,用来传递参数
1 /// <summary> 2 /// 每页行数下拉列表选项 3 /// </summary> 4 public Collection<string> RowsSelectItems 5 { 6 get { return _SelectItems; } 7 set 8 { 9 _SelectItems = value; 10 cbeRows.Properties.Items.AddRange(value); 11 cbeRows.SelectedIndex = 0; 12 RowsPerPage = int.Parse(_SelectItems[0]); 13 } 14 } 15 16 /// <summary> 17 /// 总行数 18 /// </summary> 19 public int TotalRows 20 { 21 set 22 { 23 _Rows = value; 24 _TotalPages = (int) Math.Ceiling((decimal) _Rows/RowsPerPage); 25 Refresh(); 26 } 27 } 28 29 /// <summary> 30 /// 当前选中行Handle 31 /// </summary> 32 public int FocusedRowHandle 33 { 34 private get { return _Handle - RowsPerPage*_Current; } 35 set { _Handle = RowsPerPage*_Current + value; } 36 } 37 38 /// <summary> 39 /// 每页行数 40 /// </summary> 41 public int RowsPerPage { get; private set; } 42 43 /// <summary> 44 /// 当前页 45 /// </summary> 46 public int CurrentPage => _Current + 1;
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 var page = _Current; 11 Refresh(); 12 13 if (_Current > page) 14 { 15 // 切换了页码需要重新加载数据 16 CurrentPageChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 17 } 18 else 19 { 20 TotalRowsChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 21 } 22 } 23 24 /// <summary> 25 /// 减少列表成员 26 /// </summary> 27 /// <param name="count">减少数量,默认1个</param> 28 public void RemoveItems(int count = 1) 29 { 30 _Rows -= count; 31 if (_Handle >= _Rows) _Handle = _Rows - 1; 32 33 var page = _Current; 34 Refresh(); 35 36 if (_TotalPages == 1 || _Handle < RowsPerPage*(_TotalPages - 1) || _Current < page) 37 { 38 // 不是末页或切换了页码需要重新加载数据 39 CurrentPageChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 40 } 41 else 42 { 43 TotalRowsChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 44 } 45 }
剩下的就是内部的逻辑处理函数了
1 /// <summary> 2 /// 切换每页行数 3 /// </summary> 4 private void PageRowsChanged() 5 { 6 var change = RowsPerPage < _Rows - RowsPerPage*_Current; 7 RowsPerPage = int.Parse(cbeRows.Text); 8 RowsPerPageChanged?.Invoke(this, null); 9 10 var page = _Current; 11 Refresh(); 12 13 change = change || RowsPerPage < _Rows - RowsPerPage*_Current; 14 if (_Current == page && !change) return; 15 16 // 切换了页码或当前页显示行数变化后需要重新加载数据 17 CurrentPageChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 18 } 19 20 /// <summary> 21 /// 切换当前页 22 /// </summary> 23 /// <param name="page">页码</param> 24 private void ChangePage(int page) 25 { 26 _Handle = RowsPerPage*page; 27 28 Refresh(); 29 30 CurrentPageChanged?.Invoke(this, new PageControlEventArgs(FocusedRowHandle)); 31 } 32 33 /// <summary> 34 /// 刷新控件 35 /// </summary> 36 private new void Refresh() 37 { 38 var total = _TotalPages == 0 ? 1 : _TotalPages; 39 labRows.Text = $" 行/页 | 共 {_Rows} 行 | 分 {total} 页"; 40 labRows.Refresh(); 41 42 _Current = (int) Math.Floor((decimal) _Handle/RowsPerPage); 43 btnFirst.Enabled = _Current > 0; 44 btnPrev.Enabled = _Current > 0; 45 btnNext.Enabled = _Current < _TotalPages - 1; 46 btnLast.Enabled = _Current < _TotalPages - 1; 47 btnJump.Enabled = _TotalPages > 1; 48 49 var width = (int) Math.Log10(_Current + 1)*7 + 18; 50 btnJump.Width = width; 51 btnJump.Text = CurrentPage.ToString(); 52 labRows.Focus(); 53 }
完整代码见:https://github.com/xuanbg/Utility/tree/master/Controls
经过重构后,分页控件对外仅暴露5个属性和2个方法。使用者只需要在参数变化后给相应属性赋值即可,每页行数的调整、加载列表数据和列表的FocusedRowHandle都通过订阅事件完成。代码示例如下:
1 View.TabRole.RowsPerPageChanged += (sender, args) => _PageRows = View.TabRole.RowsPerPage; 2 View.TabRole.CurrentPageChanged += (sender, args) => PageChanged(args.RowHandle); 3 View.TabRole.TotalRowsChanged += (sender, args) => View.GdvRole.FocusedRowHandle = args.RowHandle; 4 5 /// <summary> 6 /// 切换页码后重新加载角色列表 7 /// </summary> 8 /// <param name="handel">当前焦点行</param> 9 private void PageChanged(int handel) 10 { 11 _CurrentPage = View.TabRole.CurrentPage; 12 13 LoadRoles(handel); 14 } 15 16 /// <summary> 17 /// 新增角色到角色列表 18 /// </summary> 19 /// <param name="role">RoleInfo</param> 20 internal void AddRole(RoleInfo role) 21 { 22 _Roles.Add(role); 23 24 View.TabRole.AddItems(); 25 View.GrdRole.RefreshDataSource(); 26 } 27 28 /// <summary> 29 /// 删除当前所选角色 30 /// </summary> 31 internal void RoleDelete() 32 { 33 _Roles.Remove(Role); 34 35 View.TabRole.RemoveItems(); 36 View.GdvRole.RefreshData(); 37 }
如果这篇文字对看官有点用处的话,请帮忙点下推荐,谢谢!