雖然分頁控件滿天飛,因為實在沒找到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);View Code
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;
View Code
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 }
View Code
剩下的就是內部的邏輯處理函數了

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 }
View Code
完整代碼見: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 }
View Code
如果這篇文字對看官有點用處的話,請幫忙點下推薦,謝謝!