起因
最初是一位 FineUI 網友對購物車功能的需求,需要根據產品單價和數量來計算所有選中商品的總價。
這個邏輯最好在前臺使用JavaScript實現,如果把這個邏輯移動到後臺C#實現,則會導致過多的AJAX請求而影響使用者體驗。
最終效果
準備資料
在生成頁面之前,我們需要準備購物車的資料,這裡只是簡單的用表格來模擬資料:
1 protected DataTable GetCartDataTable() 2 { 3 DataTable table = new DataTable(); 4 table.Columns.Add(new DataColumn("Id", typeof(int))); 5 table.Columns.Add(new DataColumn("Code", typeof(String))); 6 table.Columns.Add(new DataColumn("Name", typeof(String))); 7 table.Columns.Add(new DataColumn("Desc", typeof(String))); 8 table.Columns.Add(new DataColumn("Price", typeof(float))); 9 table.Columns.Add(new DataColumn("Number", typeof(int))); 10 11 DataRow row = table.NewRow(); 12 row[0] = 101; 13 row[1] = "100022"; 14 row[2] = "商品一"; 15 row[3] = "這是商品一的介紹"; 16 row[4] = 35.5; 17 row[5] = 1; 18 table.Rows.Add(row); 19 20 row = table.NewRow(); 21 row[0] = 102; 22 row[1] = "100023"; 23 row[2] = "商品二"; 24 row[3] = "這是商品二的介紹"; 25 row[4] = 18.99; 26 row[5] = 2; 27 table.Rows.Add(row); 28 29 row = table.NewRow(); 30 row[0] = 103; 31 row[1] = "100024"; 32 row[2] = "商品三"; 33 row[3] = "這是商品三的介紹"; 34 row[4] = 18.99; 35 row[5] = 2; 36 table.Rows.Add(row); 37 38 row = table.NewRow(); 39 row[0] = 104; 40 row[1] = "100025"; 41 row[2] = "商品四"; 42 row[3] = "這是商品四的介紹"; 43 row[4] = 22.00; 44 row[5] = 1; 45 table.Rows.Add(row); 46 47 return table; 48 }
頁面標籤
前臺頁面使用了VBox佈局,用來實現底部彙總皮膚的高度固定,頂部表格的高度自適應頁面高度的佈局:
1 <f:PageManager ID="PageManager1" AutoSizePanelID="Panel2" runat="server" /> 2 <f:Panel ID="Panel2" runat="server" ShowBorder="false" Layout="VBox" BoxConfigAlign="Stretch" 3 BoxConfigPosition="Start" BoxConfigPadding="5" BoxConfigChildMargin="0 5 0 0" 4 ShowHeader="false"> 5 <Items> 6 <f:Grid ID="Grid1" ShowBorder="true" BoxFlex="1" ShowHeader="true" Title="購物車" 7 EnableCollapse="true" runat="server" EnableCheckBoxSelect="true" CheckBoxSelectOnly="true" 8 DataKeyNames="Id,Code,Name" EnableTextSelection="true"> 9 10 </f:Grid> 11 <f:ContentPanel runat="server" CssClass="totalpanel" ShowBorder="true" ShowHeader="false"> 12 13 </f:ContentPanel> 14 </Items> 15 </f:Panel>
VBox佈局和HBox佈局對於 FineUI 來說舉足輕重,如果你還搞不清楚其中的引數含義,請移步FineUI教程。
這裡有個小技巧,由於上下兩個皮膚緊貼在一起,所以中間的兩個邊框就顯得不好看了,我們只需透過簡單的CSS來調整,使得下面皮膚的頂部邊框寬度為零:
1 <style> 2 .totalpanel .x-panel-body { 3 border-top-width: 0 !important; 4 } 5 </style>
下面來看錶格的定義:
1 <f:Grid> 2 <Columns> 3 <f:RowNumberField /> 4 <f:BoundField Width="120px" DataField="Code" DataFormatString="{0}" HeaderText="商品程式碼" /> 5 <f:BoundField DataField="Name" ExpandUnusedSpace="true" DataFormatString="{0}" HeaderText="商品名稱" /> 6 <f:BoundField Width="120px" DataField="Price" HeaderText="商品單價" DataFormatString="¥{0:F}" /> 7 <f:TemplateField HeaderText="數量" Width="120px"> 8 <ItemTemplate> 9 <input type="hidden" class="price" runat="server" value='<%# Eval("Price") %>' /> 10 <asp:TextBox runat="server" Width="98%" ID="tbxNumber" CssClass="number" 11 TabIndex='<%# Container.DataItemIndex + 10 %>' Text='<%# Eval("Number") %>'></asp:TextBox> 12 </ItemTemplate> 13 </f:TemplateField> 14 <f:TemplateField HeaderText="小計" Width="120px"> 15 <ItemTemplate> 16 <asp:Label runat="server" CssClass="xiaoji" Text='<%# "¥" + GetXiaoji(Eval("Price"), Eval("Number")) %>'></asp:Label> 17 </ItemTemplate> 18 </f:TemplateField> 19 </Columns> 20 </f:Grid>
一些小技巧:
- DataFormatString="¥{0:F}" 將浮點數格式化為兩個小數位的字串。
- Container.DataItemIndex 表示當前項的序號,設定TabIndex是為了啟用Tab鍵導航
- 隱藏欄位 class="price",是為了方便客戶端使用JavaScript獲取產品單價
- 數量的文字輸入框的 CssClass="number" 同樣是為了方便客戶端呼叫
- 透過後臺定義的C#函式 GetXiaoji 來計算初始產品價格小計
下面來看下 GetXiaoji 的定義:
1 protected string GetXiaoji(object priceobj, object numberobj) 2 { 3 float price = Convert.ToSingle(priceobj); 4 int number = Convert.ToInt32(numberobj); 5 6 return String.Format("{0:F}", price * number); 7 }
接下來看下彙總皮膚的標籤定義:
1 <f:ContentPanel> 2 <div style="text-align: right; margin: 10px;"> 3 <div style="margin-bottom: 10px;"> 4 <input type="hidden" id="TOTAL_NUMBER" name="TOTAL_NUMBER" /> 5 <span id="totalNumber" style="color: red;"></span> 6 件商品 7 </div> 8 <div style="margin-bottom: 10px;"> 9 <input type="hidden" id="TOTAL_PRICE" name="TOTAL_PRICE" /> 10 總計:<span id="totalPrice" style="color: red; font-size: 1.5em; font-weight: bold;"></span> 11 </div> 12 <div> 13 <f:Button runat="server" Text="去結算" Enabled="false" Size="Large" ID="btnGotoPay" OnClick="btnGotoPay_Click"></f:Button> 14 </div> 15 </div> 16 </f:ContentPanel>
這裡面的幾個小技巧:
- 隱藏欄位 TOTAL_NUMBER 和 TOTAL_PRICE 是為了方便在後臺獲取總價和商品總數
- 預設設定提交按鈕的 Enabled="false",在使用者更改選中商品數量時來決定是否禁用
前臺JavaScript邏輯
FineUI 雖然號稱 No JavaScript,但這裡的真正意思是 80% 的應用場景不需要使用 JavaScript 就能輕鬆實現。
對於購物車這種需要前臺互動的頁面,還是需要開發者有一定的指令碼編寫功底。下面先羅列一下全部的JavaScript程式碼:
1 var gridClientID = '<%= Grid1.ClientID %>'; 2 var btnGotoPayClientID = '<%= btnGotoPay.ClientID %>'; 3 var numberSelector = '.f-grid-tpl input.number'; 4 var priceSelector = '.f-grid-tpl input.price'; 5 6 function getRowNumber(row) { 7 return parseInt(row.find(numberSelector).val(), 10); 8 } 9 function getRowPrice(row) { 10 return parseFloat(row.find(priceSelector).val()); 11 } 12 13 function updateTotal() { 14 var grid = F(gridClientID); 15 var selection = grid.getSelectionModel().getSelection(); 16 var store = grid.getStore(); 17 18 var total = 0; 19 $.each(selection, function (index, item) { 20 var rowIndex = store.indexOf(item); 21 var row = $(grid.body.el.dom).find('.x-grid-row').eq(rowIndex); 22 total += getRowNumber(row) * getRowPrice(row); 23 }); 24 25 $('#totalNumber').text(selection.length); 26 $('#totalPrice').text("¥" + total.toFixed(2)); 27 28 $('#TOTAL_NUMBER').val(selection.length); 29 $('#TOTAL_PRICE').val(total.toFixed(2)); 30 31 var gotoPayBtn = F(btnGotoPayClientID); 32 if (total === 0) { 33 gotoPayBtn.disable(); 34 } else { 35 gotoPayBtn.enable(); 36 } 37 } 38 39 function registerNumberChangeEvents() { 40 var grid = F(gridClientID); 41 42 // 數量改變事件 43 // http://stackoverflow.com/questions/17384218/jquery-input-event 44 $(grid.el.dom).find(numberSelector).on('input propertychange', function (evt) { 45 var $this = $(this); 46 47 var row = $this.parents('.x-grid-row'); 48 var number = getRowNumber(row); 49 var price = getRowPrice(row); 50 var resultNode = row.find('.f-grid-tpl span.xiaoji'); 51 52 resultNode.text("¥" + (number * price).toFixed(2)); 53 54 updateTotal(); 55 }); 56 } 57 58 function registerSelectionChangeEvents() { 59 var grid = F(gridClientID); 60 61 grid.on('selectionchange', function (cmp, selected) { 62 updateTotal(); 63 }); 64 } 65 66 // 頁面第一次載入完成後呼叫的函式 67 F.ready(function () { 68 registerNumberChangeEvents(); 69 registerSelectionChangeEvents(); 70 updateTotal(); 71 });
這裡只給出一些小技巧的提醒:
- F.ready 用來初始化所有需要的JavaScript程式碼,包含對 updateTotal 的呼叫
- registerNumberChangeEvents 註冊數量文字框改變的處理函式
- 文字框的 input 事件用來監視文字框的內容變化,包含鍵盤輸入、複製貼上等,IE8不支援此事件但可以使用 propertychange 代替
- registerSelectionChangeEvents 註冊使用者選中商品行改變的事件處理函式
- updateTotal 中根據總價來決定是否啟用提交按鈕
後臺C#邏輯
後臺來顯示彙總資訊,對熟悉 FineUI 的網友應該來說很簡單:
1 protected void btnGotoPay_Click(object sender, EventArgs e) 2 { 3 StringBuilder sb = new StringBuilder(); 4 sb.Append("<ol>"); 5 foreach(int rowIndex in Grid1.SelectedRowIndexArray) { 6 System.Web.UI.WebControls.TextBox tbxNumber = (System.Web.UI.WebControls.TextBox)Grid1.Rows[rowIndex].FindControl("tbxNumber"); 7 8 sb.AppendFormat("<li>{0}({1})</li>", Grid1.DataKeys[rowIndex][2], tbxNumber.Text); 9 } 10 sb.Append("</ol><hr/>"); 11 12 sb.AppendFormat("共 {0} 件商品,總計 ¥{1}", Request.Form["TOTAL_NUMBER"], Request.Form["TOTAL_PRICE"]); 13 14 Alert.Show(sb.ToString(), MessageBoxIcon.Information); 15 }
原始碼免費下載
這個簡直就是廢話!
這個示例會出現在下個版本的 FineUI(開源版)中,不過目前你可以直接從微軟的 codeplex 網站下載全部原始碼:
https://fineui.codeplex.com/SourceControl/list/changesets
24 張專業版截圖
推薦本文
如果本文對你有一定的啟發或幫助,請點選好文要頂。你也可以透過關注本部落格來及時獲取 FineUI 的最新資訊。
《FineUI小技巧》系列文章目錄
- FineUI小技巧(1)簡單的購物車頁面
- FineUI小技巧(2)將表單內全部欄位禁用、只讀、設定無效標識
- FineUI小技巧(3)表格匯出與檔案下載
- FineUI小技巧(4)關閉窗體那些事
- FineUI小技巧(5)向子視窗傳值,向父視窗傳值