[ASP.NET MVC + Vue.js] 動態問卷表單製作教學
動態表單是由程式生成使用者輸入的表單,依照需求不同,可以靈活產生不同的輸入選項。
最經典的案例是問卷表單,依題目的不同,可自由產生文字方塊、單選題或多選題的問卷表單。
許多人都用過 Google 表單,可以自由設計題目以及回答的選項,我們也可以在 ASP.NET MVC 製作出類似的表單。
在本次教學裡面,你將會製作一個動態問卷表單,題目是由資料庫來管理,我會建立題目和選項的資料表,新增一些測試資料,再由程式產生動態問卷表單。
等待使用者輸入完成後,由程式取得表單資料,最後存入資料表裡面。
接下來就以這個功能來實作一下,在文末會提供完整範例下載喔。
Contents
建立專案
開啟 Visual Studio 2022,建立新專案為「ASP.NET Web 應用程式 (.NET Framework)」。
輸入專案名稱、位置之後,選擇「MVC」範本,就可以建立專案。
建立資料表
這裡用 SQL Server 做教學資料庫,尚未安裝 SQL Server 的話,可參考這篇文章: Windows Server 如何安裝 SQL Server 2019 免費開發版
開啟資料庫,我已建好教學資料庫 “Teach”,繼續建立新 Table,以下是新 Table Schema。
問卷題目
存放每一個題目的標題,還有回答類型。
1 2 3 4 5 6 7 8 9 |
CREATE TABLE [dbo].[QuestForm]( [QuestNo] [smallint] NOT NULL, [QuestTitle] [nvarchar](50) NOT NULL, [QuestOptionType] [varchar](10) NULL, CONSTRAINT [PK_QuestTitle] PRIMARY KEY CLUSTERED ( [QuestNo] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] |
問卷選項
如果是單選題或多選題就需要在此 Table 添加選項,額外增加一個欄位 “QuestOptionWithTextbox” 給使用者自行輸入,就像很多選項的「其他」一樣。
1 2 3 4 5 6 7 8 9 10 11 |
CREATE TABLE [dbo].[QuestOption]( [QuestNo] [smallint] NOT NULL, [QuestOtpionId] [smallint] NOT NULL, [QuestOptionText] [nvarchar](50) NULL, [QuestOptionWithTextbox] [bit] NOT NULL, CONSTRAINT [PK_QuestOption] PRIMARY KEY CLUSTERED ( [QuestNo] ASC, [QuestOtpionId] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 100, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] |
使用者答案
儲存使用者回答後的答案,如果是多選題就利用分隔符號 “|” 來組合答案,如果是附加文字的輸入,也是用分隔符號 “:” 來組合答案。
1 2 3 4 5 6 7 8 9 10 11 12 |
CREATE TABLE [dbo].[QuestUserAnswer]( [QuestNo] [smallint] NOT NULL, [UserId] [varchar](10) NOT NULL, [AnswerTime] [datetime] NOT NULL, [UserAnswer] [varchar](50) NOT NULL, CONSTRAINT [PK_QuestUserAnswer] PRIMARY KEY CLUSTERED ( [QuestNo] ASC, [UserId] ASC, [AnswerTime] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY] ) ON [PRIMARY] |
建立測試資料
這裡就預先輸入一些測試題目及選項。
問卷題目
1 2 3 |
insert into [dbo].[QuestForm]([QuestNo],[QuestTitle],[QuestOptionType]) values (1,N'請問你的大名?','Textbox') insert into [dbo].[QuestForm]([QuestNo],[QuestTitle],[QuestOptionType]) values (2,N'最常使用的作業系統?','Radio') insert into [dbo].[QuestForm]([QuestNo],[QuestTitle],[QuestOptionType]) values (3,N'擅長的程式語言?','Checkbox') |
問卷選項
1 2 3 4 5 6 7 8 |
insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (2,1,N'Windows',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (2,2,N'macOS',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (2,3,N'Linux',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (2,4,N'其他',1) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (3,1,N'.Net',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (3,2,N'Java',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (3,3,N'PHP',0) insert into [dbo].[QuestOption]([QuestNo],[QuestOtpionId],[QuestOptionText],[QuestOptionWithTextbox]) values (3,4,N'其他',1) |
Controller 讀取問卷表單
這裡我們在 Controller 讀取資料庫的題目及選項,顯示在畫面上。
我直接在預設的 HomeController 的 Index() 裡面編寫程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// 資料庫連線字串 string cnStr = System.Web.Configuration.WebConfigurationManager.ConnectionStrings["ConnDB"].ConnectionString; // 輸出物件 List<QuestForm> outModel = new List<QuestForm>(); using (var cn = new SqlConnection(cnStr)) { // 取得問卷題目 var questTitle = cn.Query<QuestForm>("SELECT * FROM QuestForm ORDER BY QuestNo"); // 取得問卷選項 var questOtion = cn.Query<QuestOption>("SELECT * FROM QuestOption ORDER BY QuestNo,QuestOtpionId"); foreach (var item in questTitle) { // 建立問卷題目+選項 QuestForm form = new QuestForm(); form.QuestNo = item.QuestNo; form.QuestTitle = item.QuestTitle; form.QuestOptionType = item.QuestOptionType; // 加入問卷選項 form.QuestOptions = questOtion.Where(x => x.QuestNo == item.QuestNo).ToList(); outModel.Add(form); } } // 轉為 Json 傳至前端 ViewData["QuestForm"] = JsonConvert.SerializeObject(outModel); |
這裡使用到 Dapper 套件查詢資料庫,Dapper 是一個 ORM 物件關聯查詢套件,可以直接將資料庫查詢結果自動轉成物件。
關於更多 Dapper 的語法,可參考「黑暗執行緒」的文章教學。
資料庫連線設定
資料庫連線設定放在 Web.config 裡面,可以加入以下語法設定連線字串。
1 2 3 |
<connectionStrings> <add name="ConnDB" connectionString="Data Source=127.0.0.1;Initial Catalog=Teach;Persist Security Info=false;User ID=test;Password=test;" providerName="System.Data.SqlClient"/> </connectionStrings> |
建立問卷 Models
在剛剛程式裡面有用到新的 Model 是「QuestForm」與「QuestOption」,需要對應的 Model 來存放資料。
在「Models > 加入 > 類別」增加一個類別。
類別名稱叫「QuestModels」,接著在類別裡面增加兩個新類別。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class QuestForm { public short QuestNo { get; set; } public string QuestTitle { get; set; } public string QuestOptionType { get; set; } public List<QuestOption> QuestOptions { get; set; } } public class QuestOption { public short QuestNo { get; set; } public short QuestOtpionId { get; set; } public string QuestOptionText { get; set; } public string QuestOptionWithTextbox { get; set; } } |
加入 Dapper 套件
開啟專案的「參考 > 管理 NuGet 套件」。
搜尋 “Dapper” 找到最新的「Dapper」套件安裝。
安裝完成後,就可在引用錯誤的語法上加入 using Dapper;
語法。
View 顯示動態問卷表單
這裡直接在 \Views\Home\Index.cshtml 增加語法,先清空預設的程式碼,然後加入以下語法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<h1>動態問卷表單開發教學</h1> <div id="app"> <div class="form-group" v-for="title in QuestForm"> <div>{{title.QuestNo}} {{title.QuestTitle}}</div> <!--顯示文字方塊--> <div v-if="title.QuestOptionType === 'Textbox'"> <input type="text" class="form-control" v-bind:id="'Quest_' + title.QuestNo" /> </div> <!--顯示單選題--> <div v-if="title.QuestOptionType === 'Radio'"> <label class="radio-inline" v-for="opt in title.QuestOptions"> <input type="radio" v-bind:name="'Quest_' + title.QuestNo" v-bind:id="'Quest_' + title.QuestNo + '_' + opt.QuestOtpionId" v-bind:value="opt.QuestOtpionId" />{{opt.QuestOptionText}} <input type="text" v-bind:id="'Quest_' + title.QuestNo + '_' + opt.QuestOtpionId + '_Text'" v-if="opt.QuestOptionWithTextbox === 'True'" /> </label> </div> <!--顯示複選題--> <div v-if="title.QuestOptionType === 'Checkbox'"> <label class="checkbox-inline" v-for="opt in title.QuestOptions"> <input type="checkbox" v-bind:name="'Quest_' + title.QuestNo" v-bind:id="'Quest_' + title.QuestNo + '_' + opt.QuestOtpionId" v-bind:value="opt.QuestOtpionId" />{{opt.QuestOptionText}} <input type="text" v-bind:id="'Quest_' + title.QuestNo + '_' + opt.QuestOtpionId + '_Text'" v-if="opt.QuestOptionWithTextbox === 'True'" /> </label> </div> </div> <button type="button" class="btn btn-primary" v-on:click="SendQuest()">送出</button> </div> |
此段語法是 Html 語法加上 Vue.js 的控制語法,可以依照陣列數量,來顯示問卷題目和選項。
語法 v-for="title in QuestForm"
是 Vue.js 的迴圈用法,點此參考官方教學。
語法 v-if="title.QuestOptionType === 'Textbox'"
是 Vue.js 的條件用法,點此參考官方教學。
語法 v-on:click="SendQuest()"
可呼叫 Vue.js 的方法。
產生動態表單許多人做法會將 Html 寫在後端的語法裡面,但這裡我的做法是保持 Html 在前端,只要善加利用 for 迴圈與 if 條件就可以靈活顯示動態元件。
引用 Vue3 前端套件
我們這次範例用的前端套件是 Vue3 這是 Vue.js 的最新版本,打開 \Views\Shared\_Layout.cshtml。
在 @Scripts.Render("~/bundles/bootstrap")
底下加入 Vue.js 套件 <script src="https://unpkg.com/vue@next"></script>
。
引用完成後,就可以開始在 View 裡面編寫 Vue.js 語法。
有關 Vue3 的 CDN 引用語法可至官網文件查詢。
編寫 Vue3 語法
接著在 \Views\Home\Index.cshtml 底下繼續加入以下語法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
@section scripts{ <script> const app = { data() { return { // 問卷表單 QuestForm: [] // 使用者答案 , QuestUserAnswer: [] } }, mounted() { // 將 Controller 的 Json 轉為物件 this.QuestForm = JSON.parse('@Html.Raw(ViewData["QuestForm"])'); }, methods: { // 送出表單 SendQuest() { // 清空使用者答案 this.QuestUserAnswer = []; for (i = 0; i < this.QuestForm.length; i++) { let userAnswer = ''; // 文字方塊 if (this.QuestForm[i].QuestOptionType == 'Textbox') { userAnswer = $('#Quest_' + this.QuestForm[i].QuestNo).val(); } // 單選題 & 複選題 if (this.QuestForm[i].QuestOptionType == 'Radio' || this.QuestForm[i].QuestOptionType == 'Checkbox') { for (j = 0; j < this.QuestForm[i].QuestOptions.length; j++) { let optionId = '#Quest_' + this.QuestForm[i].QuestNo + '_' + this.QuestForm[i].QuestOptions[j].QuestOtpionId; if ($(optionId).is(":checked") == true) { // 有勾選 if (userAnswer != '') { userAnswer += '|'; } userAnswer += $(optionId).val(); // 附加文字 if ($(optionId).is(":checked") == true && this.QuestForm[i].QuestOptions[j].QuestOptionWithTextbox == 'True') { if ($(optionId + '_Text').val() == '') { alert("第 " + this.QuestForm[i].QuestNo + " 題附加文字尚未回答"); return; } userAnswer += ":" + $(optionId + '_Text').val().replace(':', ':').replace('|', '|'); } } } } // 檢查是否回答 if (userAnswer == '') { alert("第 " + this.QuestForm[i].QuestNo + " 題尚未回答"); return; } // 記錄使用者答案 let answerObj = { 'QuestNo': this.QuestForm[i].QuestNo, 'UserAnswer': userAnswer }; this.QuestUserAnswer.push(answerObj); } // 組合表單資料 var postData = {}; postData['QuestUserAnswer'] = this.QuestUserAnswer; // 傳送資料至 Controller $.ajax({ url:'@Url.Action("SaveUserAnswer", "Home")', method:'POST', dataType:'json', data: { inModel: postData, __RequestVerificationToken: $('@Html.AntiForgeryToken()').val() }, success: function (datas) { if (datas.ErrMsg) { alert(datas.ErrMsg); return; } alert(datas.ResultMsg); }, error: function (err) { alert(err.responseText); } }); } } } Vue.createApp(app).mount('#app'); </script> } |
語法 mounted()
是 Vue3 的生命週期事件,當實例掛載完成後觸發,可參考官方教學。
語法 $('#Quest_' + this.QuestForm[i].QuestNo).val()
是取得欄位值的語法,因為欄位是動態產生的,所以我用了 jQuery 的語法來取值,
語法 let answerObj = { 'QuestNo': this.QuestForm[i].QuestNo, 'UserAnswer': userAnswer };
是要傳送到後端的資料,都建議以物件方式儲存,明確記錄 Key 與 Value,要避免使用陣列順序來儲存,以免後續維護時容易改錯。
語法 __RequestVerificationToken: $('@Html.AntiForgeryToken()').val()
是 ASP.NET MVC 防止跨網域攻擊的防護方法。
儲存使用者答案
當使用者按下傳送後,就會呼叫 HomeController 的 SaveUserAnswer() 方法,這裡就建立此方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
/// <summary> /// 儲存使用者答案 /// </summary> /// <returns></returns> [ValidateAntiForgeryToken] public ActionResult SaveUserAnswer(UserAnswerModel inModel) { Hashtable outModel = new Hashtable(); // 檢查輸入來源 if (inModel.QuestUserAnswer.Count == 0) { outModel["ErrMsg"] = "無使用者答案"; return Json(outModel); } // 取得資料庫連線字串 string connStr = System.Web.Configuration.WebConfigurationManager.ConnectionStrings["ConnDB"].ConnectionString; // 當程式碼離開 using 區塊時,會自動關閉連接 using (SqlConnection conn = new SqlConnection(connStr)) { // 資料庫連線 conn.Open(); //測試使用者ID string testUserId = "Mars"; // 填寫問卷時間 string AnswerTime = DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss"); foreach (UserAnswerObj userAnswer in inModel.QuestUserAnswer) { // 寫入使用者答案 string sql = @"INSERT INTO QuestUserAnswer(QuestNo ,UserId ,AnswerTime ,UserAnswer) VALUES (@QuestNo, @UserId ,@AnswerTime ,@UserAnswer)"; SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandText = sql; // 使用參數化填值 cmd.Parameters.AddWithValue("@QuestNo", userAnswer.QuestNo); cmd.Parameters.AddWithValue("@UserId", testUserId); cmd.Parameters.AddWithValue("@AnswerTime", AnswerTime); cmd.Parameters.AddWithValue("@UserAnswer", userAnswer.UserAnswer); // 執行資料庫更新動作 cmd.ExecuteNonQuery(); } } outModel["ResultMsg"] = "儲存完成"; // 回傳 Json 給前端 return Json(outModel); } |
此段語法主要是取得前端傳來的資料寫入資料庫裡面。
新增 Model
SaveUserAnswer()
方法的參數物件要在 \Models\QuestModels.cs 裡面建立。
1 2 3 4 5 6 7 8 9 10 |
public class UserAnswerModel { public List<UserAnswerObj> QuestUserAnswer { get; set; } } public class UserAnswerObj { public short QuestNo { get; set; } public string UserAnswer { get; set; } } |
測試功能
寫到這裡,所有的語法就完成了,接下來就按 F5 來執行一下專案。
你可以一邊測試一邊 Debug 比較好理解動態表單的運作原理喔。
送出表單後,就可以到資料表 QuestUserAnswer 裡面看一下寫入的資料。
範例下載
推薦課程
相關學習文章
- [ASP.NET MVC] 多國語系切換 – 使用資料庫管理與兼容 JavaScript 顯示(附範例)
- [ASP.NET MVC] 產生 Bootstrap + Vue.js 多層式選單範本教學 (附範例)
- [ASP.NET MVC] 系統 Log 設計思維與實作自動化 Log 系統 (附範例)
如果你在學習上有不懂的地方,需要諮詢服務,可以參考站長服務,我想辨法解決你的問題
如果文章內容有過時、不適用或錯誤的地方,幫我在下方留言通知我一下,謝謝