雖然分頁控件滿天飛,因為實在沒找到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個屬性。無論是做成什么樣子,用什么語言,都是如此。
如果這篇文字對看官有點用處的話,請幫忙點下推薦,謝謝!