分享一個分頁控件的實現思路


雖然分頁控件滿天飛,因為實在沒找到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個屬性。無論是做成什么樣子,用什么語言,都是如此。

 

如果這篇文字對看官有點用處的話,請幫忙點下推薦,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM