- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودیهای کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوهی تعریف DataSource ایی که میتواند اطلاعات را ثبت، حذف و یا ویرایش کند.
در این مطلب قصد داریم به یک چنین صفحهای برسیم که در آن در ابتدای نمایش، لیست ثبت نامهای موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده میشود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیفهای جدید. در این بین مدیریت نمایش لیست ثبت نامها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.
Kendo UI MVVM
الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترلهای برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شدهاست، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانههایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شدهاند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز میباشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل میشود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده میشود.
- ViewModel واسطی است بین Model و View. کار آن انتقال دادهها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح میباشد.
- Declarative data binding جهت رهایی برنامه نویسها از نوشتن کدهای هماهنگ سازی اطلاعات المانهای View و خواص ViewModel کاربرد دارد.
در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال میکنیم.
تعریف Model و ViewModel
در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models { public class Registration { public int Id { set; get; } public string UserName { set; get; } public string CourseName { set; get; } public int Credit { set; get; } public string Email { set; get; } public string Tel { set; get; } } }
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ id: "Id", fields: { Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new. UserName: { type: 'string' }, CourseName: { type: 'string' }, Credit: { type: 'number' }, Email: { type: 'string' }, Tel: { type: 'string' } } }); }); </script>
<script type="text/javascript"> $(function () { var viewModel = kendo.observable({ accepted: false, course: new model() }); }); </script>
اتصال ViewModel به View برنامه
تعریف فرم ثبت نام را در اینجا ملاحظه میکنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگیهای خاص آن، مزین شدهاند. جزئیات آنرا در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» پیشتر بررسی کردهایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <h3>ثبت نام</h3> <ul> <li> <label for="Id">Id</label> <span id="Id" data-bind="text:course.Id"></span> </li> <li> <label for="UserName">نام</label> <input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required /> </li> <li> <label for="CourseName">دوره</label> <input type="text" dir="ltr" id="CourseName" name="CourseName" required data-bind="value:course.CourseName" /> <span class="k-invalid-msg" data-for="CourseName"></span> </li> <li> <label for="Credit">مبلغ پرداختی</label> <input id="Credit" name="Credit" type="number" min="1000" max="6000" required data-max-msg="عددی بین 1000 و 6000" dir="ltr" data-bind="value:course.Credit" class="k-textbox k-input" /> <span class="k-invalid-msg" data-for="Credit"></span> </li> <li> <label for="Email">پست الکترونیک</label> <input type="email" id="Email" dir="ltr" name="Email" data-bind="value:course.Email" required class="k-textbox" /> </li> <li> <label for="Tel">تلفن</label> <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}" required class="k-textbox" data-bind="value:course.Tel" data-pattern-msg="8 رقم" /> </li> <li> <input type="checkbox" name="Accept" data-bind="checked:accepted" required /> شرایط دوره را قبول دارم. <span class="k-invalid-msg" data-for="Accept"></span> </li> <li> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button> </li> </ul> <span id="doneMsg"></span> </form> </div>
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ // ... }); var viewModel = kendo.observable({ // ... }); kendo.bind($("#coursesSection"), viewModel); }); </script>
<input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required />
بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهلهای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترلهای برنامه، تنها کافی است ویژگیهای data-bind ایی را به آنها اضافه کنیم.
در ناحیهی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شدهاست (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کردهایم:
<input type="checkbox" name="Accept" data-bind="checked:accepted" required /> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button>
ارسال دادههای تغییر کردهی ViewModel به سرور
تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شدهاند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المانهای فرم متصل میکند نیز در ادامه اضافه شدهاست. توسط ویژگیهای data-bind کار Declarative data binding انجام میشود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ //... }); var dataSource = new kendo.data.DataSource({ type: 'json', transport: { read: { url: "api/registrations", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "api/registrations", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (data, type) { // Convert to a JSON string. Without this step your content will be form encoded. return JSON.stringify(data); } }, schema: { model: model }, error: function (e) { alert(e.errorThrown); }, change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); var viewModel = kendo.observable({ //... }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
متصل کردن DataSource به ViewModel
تا اینجا DataSource ایی جهت کار با سرور تعریف شدهاست؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
<script type="text/javascript"> $(function () { $("#coursesSection").kendoValidator({ // ... }); var model = kendo.data.Model.define({ // ... }); var dataSource = new kendo.data.DataSource({ // ... }); var viewModel = kendo.observable({ accepted: false, course: new model(), doSave: function (e) { e.preventDefault(); console.log("this", this.course); var validator = $("#coursesSection").data("kendoValidator"); if (validator.validate()) { if (this.course.Id == 0) { dataSource.add(this.course); } dataSource.sync(); // push to the server this.set("course", new model()); // reset controls } }, resetModel: function (e) { e.preventDefault(); this.set("course", new model()); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
در متد doSave، ابتدا بررسی میکنیم آیا اعتبارسنجی فرم با موفقیت انجام شدهاست یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه میکنیم. در اینجا بررسی شدهاست که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت میگردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شدهاست.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن میتوان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال میکند.
متد دلخواه resetModel سبب مقدار دهی مجدد شیء course با یک وهلهی جدید از شیء model میشود. همینقدر برای پاک کردن تمامی کنترلهای صفحه کافی است.
تا اینجا دو متد جدید را در ViewModel برنامه تعریف کردهایم. در مورد نحوهی اتصال آنها به View، به کدهای دو دکمهی موجود در فرم دقت کنید:
<button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button>
مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات
در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی میگردد. نکتهی مهمی که در اینجا باید به آن دقت داشت، نحوهی بازگشت Id رکورد جدید ثبت شدهاست. اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال میکند.
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using KendoUI07.Models; namespace KendoUI07.Controllers { public class RegistrationsController : ApiController { public HttpResponseMessage Delete(int id) { var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); RegistrationsDataSource.LatestRegistrations.Remove(item); return Request.CreateResponse(HttpStatusCode.OK, item); } public IEnumerable<Registration> Get() { return RegistrationsDataSource.LatestRegistrations; } public HttpResponseMessage Post(Registration registration) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); var id = 1; var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } registration.Id = id; RegistrationsDataSource.LatestRegistrations.Add(registration); // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود return Request.CreateResponse(HttpStatusCode.Created, registration); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public HttpResponseMessage Update(int id, Registration registration) { var item = RegistrationsDataSource.LatestRegistrations .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); if (!ModelState.IsValid || id != registration.Id) return Request.CreateResponse(HttpStatusCode.BadRequest); RegistrationsDataSource.LatestRegistrations[item.Index] = registration; return Request.CreateResponse(HttpStatusCode.OK); } } }
نمایش آنی اطلاعات ثبت شده در یک لیست
ردیفهای اضافه شده به منبع داده را میتوان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <!--فرم بحث شده در ابتدای مطلب--> </form> </div> <div id="results"> <table class="metrotable"> <thead> <tr> <th>Id</th> <th>نام</th> <th>دوره</th> <th>هزینه</th> <th>ایمیل</th> <th>تلفن</th> <th></th> <th></th> </tr> </thead> <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody> <tfoot data-template="footer-template" data-bind="source: this"></tfoot> </table> <script id="row-template" type="text/x-kendo-template"> <tr> <td data-bind="text: Id"></td> <td data-bind="text: UserName"></td> <td dir="ltr" data-bind="text: CourseName"></td> <td> #: kendo.toString(get("Credit"), "c0") # </td> <td data-bind="text: Email"></td> <td data-bind="text: Tel"></td> <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td> <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td> </tr> </script> <script id="footer-template" type="text/x-kendo-template"> <tr> <td colspan="3"></td> <td> جمع کل: #: kendo.toString(totalPrice(), "c0") # </td> <td colspan="2"></td> <td></td> <td></td> </tr> </script> </div> </div>
<script type="text/javascript"> $(function () { // ... var viewModel = kendo.observable({ accepted: false, course: new model(), coursesDataSourceRows: new kendo.data.ObservableArray([]), doSave: function (e) { // ... }, resetModel: function (e) { // ... }, totalPrice: function () { var sum = 0; $.each(this.get("coursesDataSourceRows"), function (index, item) { sum += item.Credit; }); return sum; }, deleteCourse: function (e) { // the current data item is passed as the "data" field of the event argument var course = e.data; dataSource.remove(course); dataSource.sync(); // push to the server }, editCourse: function(e) { // the current data item is passed as the "data" field of the event argument var course = e.data; this.set("course", course); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه میشود تا در ناحیهی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
<script type="text/javascript"> $(function () { var dataSource = new kendo.data.DataSource({ //... change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); }); </script>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI07.zip