<script type="text/javascript"> $(function () { // var r = "12"; var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "@Url.Action("GetProducts", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', data: { param1: "dfvdf", param2: "val2" } // ارسال اطلاعات اضافی و سفارشی به سرور در حین درخواست }, create: { url: "@Url.Action("PostProduct","Home")", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url:// function (product) { "@Url.Action("UpdateProduct","Home")",//, +product.Id; //}, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (p) { return "@Url.Action("DeleteProduct","Home")/" + p.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (options) { return kendo.stringify(options); } }, schema: { parse: function (data) { return data; }, data: "Data", total: "Total", model: { id: "Id", // define the model of the data source. Required for validation and property types. fields: { "Id": { type: "number", editable: false }, //تعیین نوع فیلد برای جستجوی پویا مهم است "Name": { type: "string", validation: { required: true }, editable: true }, "Discription": { type: "string", }, "Title": { type: "string", editable: false }, "GroupName": { type: "string", }, "Link": { type: "string" } } }, batch: false, }, error: function (e) { alert(e.errorThrown.stack); }, pageSize: 5, sort: { field: "Id", dir: "desc" } }); $("#report-grid").kendoGrid({ dataSource: productsDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, columns: [ { field: "Id", title: "#" }, { field: "Name", title: "Product" } ] }); }); </script>
ASP.NET MVC #16
ELMAH و حملات XSS
- بله. همچنان ELMAH معتبر است. نکته فوق را هم اضافه کنید، کاملتر خواهد شد.
$("#JQGrid1").jqGrid({ url: "/manager/Products/OnProductDataRequested", editurl: '/manager/Products/EditProductData', mtype: "GET", datatype: "json", page: 1, sortname: 'Priority', sortorder: "desc", viewrecords: true, jsonReader: { id: "Id" }, prmNames: { id: "Id" }, colNames: ["Id","خلاصه","توضیح خلاصه","قیمت","قیمت با تخفیف","درصد کارمزد سایت","آستانه هشدار موجودی","محتوا", "عنوان","فروشگاه","گروه","تعداد موجودی", "اولویت","محصولات مکمل","تصاویر","مقادیر مشخصه محصول","قیمت","نظرات", "فعال","ویژه","موجود","برند","عملیات درجا","عملیات کامل"], colModel: [ { key: true, width: 50, name: "Id", hidden: true, search: false }, { editable: true, width: 10, name: "Abstract", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "AbstractDescription", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Value", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Discount", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "SiteWagePercentage", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "InventoryAlertLimit", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 10, name: "Context", search: true, stype: "text", editable: true, hidden: true, editrules: { edithidden: true } }, { editable: true, width: 150, name: "Title", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { name: "shopTitle", align: 'center', viewable: true, editrules: { edithidden: true }, search: true, editable: true, stype: 'select', edittype: 'select', searchoptions: { sopt: ["eq", "ne"], dataUrl: "/manager/Products/Getshop/", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.shId + '">' + this.shTitle + '</option>'; }); } return s + '</select>'; }, }, editoptions: { dataUrl: "/manager/Products/Getshop", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.shId + '">' + this.shTitle + '</option>'; }); } return s + '</select>'; }, } }, { editable: true, name: "groupTitle", search: true, stype: "select" , editrules: { edithidden: true }, search: true, edittype: 'select', editoptions: { dataUrl: "/manager/Products/GetGroups", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.grpId + '">' + this.grpTitle + '</option>'; }); } return s + '</select>'; }, } }, { editable: true, width: 70, name: "AvailableCount", search: true, stype: "number", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: true, width: 50, name: "Priority", search: true, stype: "number", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "ComplementProducts", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 70, name: "Images", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 100, name: "ProductProperty", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "Price", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: false, width: 80, name: "Comments", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, { editable: true, width: 50, name: "Active", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" } , formatoptions: { disabled: false} }, { editable: true, width: 80, name: "AmazingOffer", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" } , formatoptions: { disabled: false} }, { editable: true, width: 80, name: "Available", search: true, formatter: 'checkbox', edittype: 'checkbox', editoptions: { value: "True:False" }, searchoptions: { "sopt": ["bw", "eq"] }, formatoptions: { disabled: false } }, { name: 'BrandId', align: 'center', hidden: true, viewable: true, editrules: { edithidden: true }, editable: true, stype: 'select', edittype: 'select', editoptions: { dataUrl: "/manager/Products/GetBrands", buildSelect: function (data) { var response, s = '<select>', i; response = jQuery.parseJSON(data); if (response && response.length) { $.each(response, function (i) { s += '<option value="' + this.brandId + '">' + this.brandTitle + '</option>'; }); } return s + '</select>'; }, } }, { name: "myac", width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true } }, { editable: false, width: 70, name: "FullEdit", search: true, stype: "text", searchoptions: { "sopt": ["bw", "eq"] } }, // BLAH, BLAH, BLAH ], gridComplete: function () { var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); for (var i = 0; i < ids.length; i++) { var cl = ids[i]; ComplementProducts = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ComplementProducts/Index/" + cl + " }) + '><span class=\"fa fa-shopping-cart\" style='color: white;'></span></a></div>"; Images = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Images/Index/" + cl + " }) + '><span class=\"fa fa-picture-o\" style='color: white;'></span></a></div>"; ProductProperty = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/ProductPropertyItems/Index/" + cl + " }) + '><span class=\"fa fa-braille\" style='color: white;'></span></a></div>"; Price = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/pricesppitems/Index/" + cl + " }) + '><span class=\"fa fa-line-chart\" style='color: white;'></span></a></div>"; Comments = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Comments/" + cl + " }) + '><span class=\"fa fa-comments\" style='color: white;'></span></a></div>"; FullEdit = "<div class=\"btn-group\"><a class='btn btn-primary' href=/manager/Products/Edit/" + cl + " }) + '><span class=\"fa fa-edit\" style='color: white;'></span></a><a class='btn btn-primary' href=/manager/Products/Details/" + cl + " }) + '><span class=\"fa fa-exclamation-circle\" style='color: white;'></span></a></div>"; jQuery("#JQGrid1").jqGrid('setRowData', ids[i], { ComplementProducts: ComplementProducts, Images: Images, ProductProperty: ProductProperty, Price: Price, Comments: Comments, FullEdit: FullEdit}); } }, loadComplete: function () { var activeButton = getColumnIndexByName('Active'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (activeButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); // alert("active clicked " + rowId); $.ajax({ type: "POST", url: "/manager/Products/Activate", data: { id: rowId }, dataType: "json" }); }); var amazingOfferButton = getColumnIndexByName('AmazingOffer'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (amazingOfferButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); $.ajax({ type: "POST", url: "/manager/Products/ShowInAmazingOffer", data: { id: rowId }, dataType: "json" }); $('#JQGrid1').trigger('reloadGrid'); }); var availableButton = getColumnIndexByName('Available'); var ids = jQuery("#JQGrid1").jqGrid('getDataIDs'); //id's $("tbody > tr.jqgrow > td:nth-child(" + (availableButton + 1) + ") > input", this).click(function (e) { var rowId = $(e.target).closest("tr").attr("id"); $.ajax({ type: "POST", url: "/manager/Products/Available", data: { id: rowId }, dataType: "json" }); $('#JQGrid1').trigger('reloadGrid'); }); }, height: "auto", caption: "", viewrecords: true, rowNum: 20, direction: "rtl", pager: jQuery('#JQGrid1_pager'), rowList: [10, 20, 30, 40], toppager: true, jsonReader: { root: "rows", page: "page", total: "total", records: "records", repeatitems: false, Id: "0" }, }).jqGrid('navGrid', '#JQGrid1_pager', // the buttons to appear on the toolbar of the grid { edit: true, add: true, del: true, search: true, refresh: true, view: false, position: "left", cloneToTop: true,searchtext:"جستجو" }, // options for the Edit Dialog { width: 450, editCaption: "ویرایش محصول", recreateForm: true, closeAfterEdit: true, viewPagerButtons: false, //afterShowForm: populateGroups, errorTextFormat: function (data) { return 'Error: ' + data.responseText } }, // Add options { url: '/TabMaster/Create', closeAfterAdd: true }, // Delete options { url: '/manager/Products/Remove' }, { zIndex: 100, caption: "جستجوی محصول", sopt: ['cn'] } );
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
فرمت کردن اطلاعات نمایش داده شده به کمک jqGrid در ASP.NET MVC
استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
فرمهای پویای jqGrid نیز به صورت Ajax ایی به سرور ارسال میشوند و اگر نوع عناصر تشکیل دهندهی آنها file تعیین شوند، قادر به ارسال این فایلها به سرور نخواهند بود. در ادامه نحوهی یکپارچه سازی افزونهی AjaxFileUpload را با فرمهای jqGrid بررسی خواهیم کرد.
تغییرات فایل Layout برنامه
در اینجا دو فایل جدید ajaxfileupload.js و jquery.blockUI.js به مجموعهی فایلهای تعریف شده اضافه شدهاند:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.7.2.min.js"></script> <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script> <script src="~/Scripts/i18n/grid.locale-fa.js"></script> <script src="~/Scripts/jquery.jqGrid.src.js"></script> <script src="~/Scripts/ajaxfileupload.js"></script> <script src="~/Scripts/jquery.blockUI.js"></script> @RenderSection("Scripts", required: false) </body> </html>
PM> Install-Package jQuery.BlockUI
نکتهای در مورد واکنشگرا کردن jqGrid
اگر میخواهید عرض jqGrid به تغییرات اندازهی مرورگر پاسخ دهد، تنها کافی است تغییرات ذیل را اعمال کنید:
<div dir="rtl" id="grid1" style="width:100%;" align="center"> <div id="rsperror"></div> <table id="list" cellpadding="0" cellspacing="0"></table> <div id="pager" style="text-align:center;"></div> </div> <script type="text/javascript"> $(document).ready(function () { // Responsive jqGrid $(window).bind('resize', function () { var targetContainer = "#grid1"; var targetGrid = "#list"; $(targetGrid).setGridWidth($(targetContainer).width() - 2, true); }).trigger('resize'); $('#list').jqGrid({ caption: "آزمایش هفتم", /// ..... }).navGrid( /// ..... ).jqGrid('gridResize', { minWidth: 400, minHeight: 150 }); }); </script>
همچنین اگر میخواهید کاربر بتواند اندازهی گرید را دستی تغییر دهد، به انتهای تعاریف گرید، تعریف متد gridResize را نیز اضافه کنید.
نحوهی تعریف ستونی که قرار است فایل آپلود کند
colModel: [ { name: '@(StronglyTyped.PropertyName<Product>(x=>x.ImageName))', index: '@(StronglyTyped.PropertyName<Product>(x => x.ImageName))', align: 'center', width: 220, editable: true, edittype: 'file', formatter: function (cellvalue, options, rowObject) { return "<img src='/images/" + cellvalue + "?rnd=" + new Date().getTime() + "' />"; }, unformat: function (cellvalue, options, cell) { return $('img', cell).attr('src').replace('/images/', ''); } } ],
Rnd اضافه شده به انتهای آدرس تصویر، جهت جلوگیری از کش شدن آن تعریف شدهاست.
کتابخانهی JqGridHelper
در قسمتهای قبل مطالب بررسی jqGrid یک سری کلاس مانند JqGridData برای بازگشت اطلاعات مخصوص jqGrid و یا JqGridRequest برای دریافت پارامترهای ارسالی توسط آن به سرور، تهیه کردیم؛ به همراه کلاسهایی مانند جستجو و مرتب سازی پویای اطلاعات.
اگر این کلاسها را از پروژهها و مثالهای ارائه شده خارج کنیم، میتوان به کتابخانهی JqGridHelper رسید که فایلهای آن در پروژهی پیوست موجود هستند.
همچنین در این پروژه، کلاسی به نام StronglyTyped با متد PropertyName جهت دریافت نام رشتهای یک خاصیت تعریف شدهاست. گاهی از اوقات این تنها چیزی است که کدهای سمت کلاینت، جهت سازگار شدن با Refactoring و Strongly typed تعریف شدن نیاز دارند و نه ... محصور کنندههایی طویل و عریض که هیچگاه نمیتوانند تمام قابلیتهای یک کتابخانهی غنی جاوا اسکریپتی را به همراه داشته باشند.
با کمی جستجو، برای jqGrid نیز میتوانید از این دست محصور کنندههارا پیدا کنید اما ... هیچکدام کامل نیستند و دست آخر مجبور خواهید شد در بسیاری از موارد مستقیما JavaScript نویسی کنید.
یکپارچه سازی افزونهی AjaxFileUpload با فرمهای jqGrid
پس از این مقدمات، ستون ویژهی actions که inline edit را فعال میکند، چنین تعریفی را پیدا خواهد کرد:
colModel: [ { name: 'myac', width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true, afterSave: function (rowid, response) { doInlineUpload(response, rowid); }, delbutton: true, delOptions: { url: "@Url.Action("DeleteProduct","Home")" } } } ],
و ویژگیهای قسمتهای edit، add و delete فرمهای پویای jqGrid باید به نحو ذیل تغییر کنند:
$('#list').jqGrid({ caption: "آزمایش هفتم", // .... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { width: 'auto', reloadAfterSubmit: true, checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); }, afterSubmit: doFormUpload, closeAfterEdit: true }, //add options { width: 'auto', url: '@Url.Action("AddProduct","Home")', reloadAfterSubmit: true, checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); }, afterSubmit: doFormUpload, closeAfterAdd: true }, //delete options { url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: true }).jqGrid('gridResize', { minWidth: 400, minHeight: 150 });
افزونهی AjaxFileUpload پس از ارسال اطلاعات عناصر غیر فایلی فرم، باید فعال شود. به همین جهت است که از رویداد afterSubmit در حالت نمایش فرمهای پویا و رویداد afterSave در حالت ویرایش inline استفاده کردهایم.
در ادامه تعاریف متدهای doInlineUpload و doUpload بکار گرفته شده در رویداد afterSubmit را مشاهده میکنید:
function doInlineUpload(response, rowId) { return doUpload(response, null, rowId); } function doFormUpload(response, postdata) { return doUpload(response, postdata, null); } function doUpload(response, postdata, rowId) { // دریافت خروجی متد ثبت اطلاعات از سرور // و استفاده از آی دی رکورد ثبت شده برای انتساب فایل آپلودی به آن رکورد var result = $.parseJSON(response.responseText); if (result.success === false) return [false, "عملیات ثبت موفقیت آمیز نبود", result.id]; var fileElementId = '@(StronglyTyped.PropertyName<Product>(x=>x.ImageName))'; if (rowId) { fileElementId = rowId + "_" + fileElementId; } var val = $("#" + fileElementId).val(); if (val == '' || val === undefined) { // فایلی انتخاب نشده return [false, "لطفا فایلی را انتخاب کنید", result.id]; } $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' }); $.ajaxFileUpload({ url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود secureuri: false, fileElementId: fileElementId, // آی دی المان ورودی فایل dataType: 'json', data: { id: result.id }, // اطلاعات اضافی در صورت نیاز complete: function () { $('#grid1').unblock(); }, success: function (data, status) { $("#list").trigger("reloadGrid"); }, error: function (data, status, e) { alert(e); } }); return [true, "با تشکر!", result.id]; }
متد doUpload توسط پارامتر response، اطلاعات بازگشتی پس از ذخیره سازی متداول اطلاعات فرم را دریافت میکند. برای مثال ابتدا اطلاعات معمولی یک محصول در بانک اطلاعاتی ذخیره شده و سپس id آن به همراه یک خاصیت به نام success از طرف سرور بازگشت داده میشوند.
اگر success مساوی true بود، ادامهی کار آپلود فایل انجام خواهد شد. در اینجا ابتدا بررسی میشود که آیا فایلی از طرف کاربر انتخاب شدهاست یا خیر؟ اگر خیر، یک پیام اعتبارسنجی سفارشی به او نمایش داده خواهد شد.
خروجی متد doUpload حتما باید به شکل یک آرایه سه عضوی باشد. عضو اول آن true و false است؛ به معنای موفقیت یا عدم موفقیت عملیات. عضو دوم پیام اعتبارسنجی سفارشی است و عضو سوم، Id ردیف.
در ادامه افزونهی jQuery.BlockUI فعال میشود تا ارسال فایل به سرور را به کاربر گوشزد کند.
سپس فراخوانی متداول افزونهی ajaxFileUpload را مشاهده میکنید. تنها نکتهی مهم آن فراخوانی متد reloadGrid در حالت success است. به این ترتیب گرید را وادار میکنیم تا اطلاعات ذخیره شده در سمت سرور را دریافت کرده و سپس تصویر را به نحو صحیحی نمایش دهد.
کدهای سمت سرور آپلود فایل
[HttpPost] public ActionResult AddProduct(Product postData) { // ... return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult EditProduct(Product postData) { // ... return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); } // todo: change `imageName` according to the form's file element name [HttpPost] public ActionResult UploadFiles(HttpPostedFileBase imageName, int id) { // .... return Json(new { FileName = product.ImageName }, "text/html", JsonRequestBehavior.AllowGet); }
در حالتهای Add و Edit، نیاز است id رکورد ثبت شده بازگشت داده شود. این id در سمت کلاینت توسط پارامتر response دریافت میشود. از آن در افزونهی ارسال فایل به سرور استفاده خواهیم کرد. اگر به متد UploadFiles دقت کنید، این id را دریافت میکند. بنابراین میتوان یک ربط منطقی را بین فایل ارسالی و رکورد متناظر با آن برقرار کرد.
Content type مقدار بازگشتی از متد UploadFiles حتما باید text/html باشد (افزونهی ارسال فایلها، اینگونه کار میکند).
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
jqGrid07.zip
- بررسی ویجت Kendo UI File Upload
در مطلب قبل جزئیات استفاده از ویجت آپلود فریمورک قدرتمند Kendo UI عنوان شدند. در این مطلب قصد داریم طریقهی استفاده از آن را به صورت پاپ آپ، در ویجت گرید Kendo بررسی کنیم.
مدل زیر را در نظر بگیرید:
var product = { ProductId: 1001, ProductName: "Product 1001", Available: true, Filename: "Image02.png" };
var productCount = 100; var products = []; function datasourceFilling() { for (var i = 0; i < productCount; i++) { var product = { ProductId: i, ProductName: "Product " + i + " Name", Available: i % 2 == 0 ? true: false, Filename: i % 2 == 0 ? "Image01.png" : "Image02.png" }; products.push(product); } }
function makekendoGrid() { $("#grid").kendoGrid({ dataSource: { data: products, schema: { model: { //id: "ProductId", fields: { ProductId: { editable: false, nullable: true }, ProductName: { validation: { required: true } }, Available: { type: "boolean" }, ImageName: { type: "string", editable: false }, Filename: { type: "string", validation: { required: true } } } } }, pageSize: 20 }, height: 550, scrollable: true, sortable: true, filterable: true, pageable: { input: true, numeric: false }, editable: { mode: "popup", }, columns: [ { field: "ProductName", title: "Product Name" }, { field: "Available", width: "100px", template: '<input type="checkbox" #= Available ? checked="checked" : "" # disabled="disabled" ></input>' }, { field: "ImageName", width: "150px", template: "<img src='/img/#=Filename#' alt='#=Filename #' Title='#=Filename #' height='80' width='80'/>" }, { field: "Filename", width: "100px", editor: fileEditor }, { command: ["edit"], title: " ", width: "200px" } ] }); var grid = $('#grid').data('kendoGrid'); grid.hideColumn(3); }
function fileEditor(container, options) { $('<input type="file" name="file"/>') .appendTo(container) .kendoUpload({ multiple: false, async: { saveUrl: "@Url.Action("Save", "Home")", removeUrl: "@Url.Action("Remove", "Home")", autoUpload: true, }, upload: function (e) { alert("upload"); e.data = { Id: options.model.Id }; }, success: function (e) { alert("success"); options.model.set("ImageName", e.response.ImageUrl); }, error: function (e) { alert("error"); alert("Failed to upload " + e.files.length + " files " + e.XMLHttpRequest.status + " " + e.XMLHttpRequest.responseText); } }); }
[System.Web.Mvc.HttpPost] public virtual ActionResult Save(HttpPostedFileBase file) { var exName = Path.GetExtension(file.FileName); var totalFileName = System.Guid.NewGuid().ToString().ToLower().Replace("-", "") + exName; var physicalPath = Path.Combine(Server.MapPath("/img"), totalFileName); file.SaveAs(physicalPath); return Json(new { ImageUrl = totalFileName }, "text/plain"); } [System.Web.Mvc.HttpPost] public virtual ContentResult Remove(string fileName) { if (fileName != null) { var physicalPath = Path.Combine(Server.MapPath("/img"), fileName); System.IO.File.Delete(physicalPath); } // Return an empty string to signify success return Content(""); }
حاصل کار بصورت تصویر زیر نمایش داده شده است:
EF Code First #5
در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیتهای ردیابی تغییرات را از دست خواهیم داد و این عملیات، صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدلها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا میگردد.
هدف قرار دادن مرحلهای خاص یا لغو آن
به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:
Unable to update database to match the current model because there are pending changes and
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.
EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن میتوان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:
Add-Migration AddSomeProp2ToProject
به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}
public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}
مدلهای برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشدهاند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده میگردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .
در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:
Update-Database -Verbose
پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]
اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:
Add-Migration AddSomeProp3ToProject
Update-Database -Verbose
اینبار نیز یک کلاس جدید به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امکان به روز رسانی بانک اطلاعاتی میسر میگردد.
در ادامه برای مثال به این نتیجه رسیدهایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کردهایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه میشوند، میتوان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.
نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:
get-help update-database -detailed
به عنوان نمونه اگر در حین فراخوانی دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عملیات متوقف میشود. برای وادار کردن پروسه به انجام تغییرات بر روی بانک اطلاعاتی میتوان از پارامتر Force در اینجا استفاده کرد.
در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:
Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose
اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]
همانطور که ملاحظه میکنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمیشود و فرض بر این است که پیشتر اینکار را انجام دادهاید.
سفارشی سازی کلاسهای مهاجرت
تمام کلاسهای خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری میکنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاسهای مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، میتوان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کردهایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}
public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}
متد String در اینجا چنین امضایی دارد:
public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null,
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)
که برای نمونه در اینجا پارامتر defaultValue آنرا در کلاس AddSomeProp3ToProject مقدار دهی کردهایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:
ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'
تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شدهاست نیز میتواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونهای از آنرا در متد Up فوق مشاهده میکنید.
افزودن رکوردهای پیش فرض در حین به روز رسانی بانک اطلاعاتی
در قسمتهای قبل با متد Seed که به همراه آغاز کنندههای بانک اطلاعاتی EF ارائه شدهاند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration، چنین امری میسر است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}
متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی میکند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آنرا اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی میکند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص میشود که بر اساس چه خاصیتی باید در مورد update یا add تصمیمگیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object میتوان کمک گرفت new { p.Name, p.LastName } .
تولید اسکریپت به روز رسانی بانک اطلاعاتی
بهترین کار و امنترین روش حین انجام این نوع به روز رسانیها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس میتوان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداولتری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script
پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:
ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')
همانطور که ملاحظه میکنید، در یک مرحله، جدول پروژهها را به روز خواهد کرد و در مرحله بعد، سابقه آنرا در جدول __MigrationHistory ثبت میکند.
یک نکته:
اگر دستور فوق را بر روی برنامهای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور میتوان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:
Update-Database -Verbose -Script -SourceMigration:"stepName"
استفاده از DB Migrations در عمل
البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations، فایلهای اولیه DB Migration را ایجاد کنید. پیش فرضهای آن را نیز تغییر ندهید.
ج) هر بار که کلاسهای مدل برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script
به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید میگردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).
ایراد در نمایش جزییات نمایش مطالب و TreeView Extension
خطای دریافتی بعد از زدن روی دکمه مشاهده
( به ازای کامنت هر مطلب از TreeView استفاده شده، خطا میدهد)
HttpCompileException: d:\IrisCms-master\Iris.Web\Views\Comment\_PostComments.cshtml(7): error CS1061: 'System.Web.Mvc.HtmlHelper<System.Collections.Generic.IEnumerable<Iris.DomainClasses.Entities.Comment>>' does not contain a definition for 'TreeView' and no extension method 'TreeView' accepting a first argument of type 'System.Web.Mvc.HtmlHelper<System.Collections.Generic.IEnumerable<Iris.DomainClasses.Entities.Comment>>' could be found (are you missing a using directive or an assembly reference?)
using System.Collections.Generic; using System.Web.Mvc; using Iris.Servicelayer.Interfaces; namespace Iris.Web.Controllers { public partial class TreeViewController : Controller { private readonly IPageService _pageService; public TreeViewController(IPageService pageService) { _pageService = pageService; } public virtual ActionResult Index() { List<TreeViewLocation> locations = GetLocations(); return View(_pageService.GetNavBarData(x => x.Body.Equals("dsad"))); } public static List<TreeViewLocation> GetLocations() { var locations = new List<TreeViewLocation> { new TreeViewLocation { Name = "United States", ChildLocations = { new TreeViewLocation { Name = "Chicago", ChildLocations = { new TreeViewLocation {Name = "Rack 1"}, new TreeViewLocation {Name = "Rack 2"}, new TreeViewLocation {Name = "Rack 3"}, new TreeViewLocation {Name = "Rack 3"}, } }, new TreeViewLocation { Name = "Dallas", ChildLocations = { new TreeViewLocation { Name = "Rack 1", ChildLocations = { new TreeViewLocation { Name = "Rack 1", ChildLocations = { new TreeViewLocation {Name = "Rack 1"}, new TreeViewLocation {Name = "Rack 2"}, new TreeViewLocation {Name = "Rack 3"}, new TreeViewLocation {Name = "Rack 3"}, } }, new TreeViewLocation {Name = "Rack 2"}, new TreeViewLocation {Name = "Rack 3"}, new TreeViewLocation {Name = "Rack 3"}, } }, new TreeViewLocation {Name = "Rack 2"}, new TreeViewLocation {Name = "Rack 3"}, new TreeViewLocation {Name = "Rack 3"}, } }, new TreeViewLocation {Name = "Dallas"}, new TreeViewLocation {Name = "Dallas"}, new TreeViewLocation {Name = "Dallas"}, new TreeViewLocation {Name = "Dallas"}, new TreeViewLocation {Name = "Dallas"}, new TreeViewLocation {Name = "Dallas"}, } }, new TreeViewLocation { Name = "Canada", ChildLocations = { new TreeViewLocation {Name = "Ontario"}, new TreeViewLocation {Name = "Windsor"} } } }; return locations; } } public class TreeViewLocation { public TreeViewLocation() { ChildLocations = new HashSet<TreeViewLocation>(); } public int Id { get; set; } public string Name { get; set; } public ICollection<TreeViewLocation> ChildLocations { get; set; } } }
هدف از مطلب فوق اجرا نمودن عملیات Insert، Update و غیرو...
بوسیله چندین Connection در یک Transaction در زمان اجرای سرویسهای WCF میباشد. برای پیاده سازی و شرح Transaction ، سه پروژه ایجاد مینماییم. دو پروژه WCF سرویس و یک پروژهClient ، هر سه پروژه را در یک Solution به نام WCFTransaction اضافه مینماییم. در هر دو پروژه WCF بطور جداگانه Connection رویDatabase ایجاد مینماییم. سپس سعی میکنیم بوسیله Transaction عملیات Insert هر دو Service را کنترل نماییم. بطوریکه اگر یکی از Service ها در زمان عملیات Insert دچار مشکل شود. دیگری نیز Commit نگردد. به عبارتی در قدیم نمیتوانستیم بیش از یک Connection در یک Transaction ایجاد نماییم. اما بوسیله Transactionscope ، انجام عملیات Insert، Update و غیرو... بوسیله چندین Connection به یکDatabase بطور همزمان در یک Transaction فراهم شده است. برای نمایش دادن عملیات Rollback نیز،به عمد خطایی ایجاد میکنیم،تا نحوه Rollback شدن در Transaction را مشاهده نماییم.
سعی شده است پیاده سازی و استفاده از Transaction در شش مرحله انجام شود.
مرحله اول: ایجاد دو پروژه WCFService و یک پروژه Client جهت فراخوانی (Call) کردن سرویسها
در این مرحله همانطور که از قیل نیز
توضیح داده شده است، دو پروژه WCF به نامهای WCFService1 و WCFService2 ایجاد شده است و یک پروژه Client به نام WCFTransactions نیز ایجاد میکنیم.
مرحله دوم : افزودن Attribute ی به نام TransactionFlow به Interface سرویسها.
در این مرحله در Interface هریک از سرویسها متد جدیدی به نام UpdateData اضافه مینماییم. که عملیات Insert into درون Database را انجام میدهد. حال بالای متد UpdateData از صفت TransactionFlow استفاده مینماییم. تا قابلیت Transaction برای متد فوق فعال گردد و متد فوق اجازه مییابد از Transaction استفاده نماید.
<ServiceContract()> _ Public Interface IService1 <OperationContract()> _ Function GetData(ByVal value As Integer) As String <OperationContract()> _ Function GetDataUsingDataContract(ByVal composite As CompositeType) As CompositeType <OperationContract()> _ <TransactionFlow(TransactionFlowOption.Allowed)> _ Sub UpdateData() End Interface
مرحله سوم:
در این مرحله متد UpdateData را پیاده سازی مینماییم. بطوریکه یک Insert Into ساده در Database انجام میدهیم.و بالای متد فوق نیز کد زیر را میافزاییم.
<OperationBehavior(TransactionScopeRequired:=True)>
کد متد UpdateData
<OperationBehavior(TransactionScopeRequired:=True)> _ Public Sub UpdateData() Implements IService1.UpdateData Dim objConnection As SqlConnection = New SqlConnection(strConnection) objConnection.Open() Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(10,10)", objConnection) objCommand.ExecuteNonQuery() objConnection.Close() End Sub
مرحله دوم و سوم را برای Service دوم نیز تکرار مینماییم.
مرحله چهارم:
در این مرحله TransactionFlow را در Web.Config دو سرویس فعال مینماییم. تا قابلیت استفاده از TransactionFlow برای سرویسها نیز فعال گردد. نحوه فعال نمودن بصورت زیر میباشد:
برای WCFService1خواهیم داشت:
<bindings> <wsHttpBinding> <binding name="TransactionalBind" transactionFlow="true"/> </wsHttpBinding> </bindings>
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="TransactionalBind" contract="WcfService1.IService1">
برای WCFService2نیز خواهیم داشت:
<bindings> <wsHttpBinding> <binding name="TransactionalBind" transactionFlow="true"/> </wsHttpBinding> </bindings>
و در ادامه داریم:
<endpoint address="" binding="wsHttpBinding" bindingConfiguration="TransactionalBind" contract="WcfService2.IService1">
مرحله پنجم:
در این مرحله دو سرویس فوق را به پروژه WCFTransactions اضافه نموده و قطعه کد زیر را درون فرم Load مینویسیم.
Private Sub frmmain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load Using ts As New TransactionScope(TransactionScopeOption.Required) Try Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client() obj.UpdateData() Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client() obj1.UpdateData() ts.Complete() Catch ex As Exception ts.Dispose() End Try End Using End Sub
پس از اجرای برنامه دو رکورد در جدول درج خواهد شد.
مرحله ششم:
حال برای RollBack کردن کل عملیات و مشاهده آنها کافیست در یکی از متدهای UpdateData یک Throw Exception ایجاد نماییم.
سعی میکنیم با کمی تغییر در متد UpdateData در WCFService2 ، خطایی ایجاد شود، تا نحوه RollBack را مشاهده نماییم.
Public Sub UpdateData() Implements IService1.UpdateData
Throw New Exception()
Dim objConnection As SqlConnection = New SqlConnection(strConnection)
objConnection.Open()
Dim objCommand As SqlCommand = New SqlCommand("insert into T(ID,Age) values(101,101)", objConnection)
objCommand.ExecuteNonQuery()
objConnection.Close()
End Sub
فقط کد زیر به متد UpdateData اضافه شده است:
Throw New Exception()
و در رویداد Load فرم نیز پیاده سازی آن بشکل زیر خواهد بود:
Using ts As New TransactionScope(TransactionScopeOption.Required) Try Dim obj As ServiceReference1.Service1Client = New ServiceReference1.Service1Client() obj.UpdateData() Throw New Exception("There was Error") Dim obj1 As ServiceReference2.Service1Client = New ServiceReference2.Service1Client() obj1.UpdateData() ts.Complete() Catch ex As Exception ts.Dispose() End Try End Using
وقتی برنامه را اجرا نمایید، مشاهده میکنید که هیچ رکوردی دورن دیتابیس درج نشده است.
بسبار مهم: برای اینکه بتوانید بصورت Distibuted عملیات Transaction را انجام دهید میبایست تنظیماتی را روی سرور که دیتایس و سرویسها و کامپیوتر کلاینت انجام دهید که بصورت زیر میباشد:
نحوه تنظیم:
1- سرویسDistribute Transaction Coordinator را روی هر دو Serverهای WCFService ، Database و کامپیوتر کلاینت، Start مینماییم.
البته در شرایطی که Serviceهای WCF و برنامه Client و Database روی یک سیستم باشد، تنظیمات فوق فقط روی همان سیستم انجام میشود.
برای دسترسی به قسمت Service های Windows ابتدا Administrative
Tools و سپس Service را باز نمایید و روی Start کلیک کنید.
2- در ادامه روی MY Computer کلیک راست نموده و تب MSDTC را انتخاب نمایید:
در ادامه روی Security
Configuration کلیک نمایید. تا فرم زیر نمایش داده شود.
مطمئن شوید که آیتمهای زیر انتخاب شده باشند:
· Network DTC Access
· Allow Remote Clients
· Allow Inbound
· Allow Outbound
·
Enable
Transaction Internet Protocol(TIP) Transactions
در ضمن اگر از SQL Server 2000 استفاده مینمایید. لازم است تنظیم زیر را انجام دهید.
روی SQL Server Service Manager کلیک نموده و کامبوی Service را Dropdown نمایید و Distribute Transaction Coordinator را انتخاب کنید. اما برای ورژنهای بالاتر از SQL Server 2000 نیاز به انتخاب Distribute Transaction Coordinator نمیباشد.
امیدوارم مطلب فوق مفید واقع شود، چنانچه کم و کاستی مشاهده نمودید، اینجانب را از نظرات خود بهره مند سازید.
در ادامه قصد داریم نحوه پیاده سازی آنرا در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.
مدل برنامه
namespace jQueryMvcSample02.Models { public class BlogPost { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
منبع داده فرضی برنامه
using System.Collections.Generic; using System.Linq; using jQueryMvcSample02.Models; namespace jQueryMvcSample02.DataSource { public static class BlogPostDataSource { private static IList<BlogPost> _cachedItems; /// <summary> /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانیها صورت خواهد گرفت /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای /// تهیه یک لیست برای تمام کاربران سایت است /// </summary> static BlogPostDataSource() { _cachedItems = createBlogPostsInMemoryDataSource(); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogPost> createBlogPostsInMemoryDataSource() { var results = new List<BlogPost>(); for (int i = 1; i < 30; i++) { results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i }); } return results; } /// <summary> /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند /// شماره صفحه از یک شروع میشود /// </summary> public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4) { var skipRecords = pageNumber * recordsPerPage; return _cachedItems .OrderByDescending(x => x.Id) .Skip(skipRecords) .Take(recordsPerPage) .ToList(); } } }
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts میباشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازهای از اطلاعات که قرار است نمایش داده شوند، دریافت میگردد. خوشبختانه این متدها معادلهای مناسبی را در اکثر بانکهای اطلاعاتی داشته و استفاده از آنها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار میکند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.
کنترلر برنامه
using System.Linq; using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample02.DataSource; using jQueryMvcSample02.Security; namespace jQueryMvcSample02.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { //آغاز کار با صفحه صفر است var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0); return View(list); //نمایش ابتدایی صفحه } [HttpPost] [AjaxOnly] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public virtual ActionResult PagedIndex(int? page) { var pageNumber = page ?? 0; var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber); if (list == null || !list.Any()) return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها return PartialView("_ItemsList", list); } [HttpGet] public ActionResult Post(int? id) { if (id == null) return Redirect("/"); //todo: show the content here return Content("Post " + id.Value); } } }
الف) یک متد که بر اساس HttpGet کار میکند. این متد در اولین بار نمایش صفحه فراخوانی میگردد و اطلاعات صفحه آغازین را نمایش میدهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستیهای AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیکهای کاربر بر روی دکمه «بیشتر» استفاده میگردد. بنابراین تنها کاری که افزونه جیکوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده میشود.
دو View برنامه
همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر میکند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost> <ul> @foreach (var item in Model) { <li> <h5> @Html.ActionLink(linkText: item.Title, actionName: "Post", controllerName: "Home", routeValues: new { id = item.Id }, htmlAttributes: null) </h5> @item.Body </li> } </ul>
@model IList<jQueryMvcSample02.Models.BlogPost> @{ ViewBag.Title = "Index"; var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home"); } <h2> اسکرول نامحدود</h2> @{ Html.RenderPartial("_ItemsList", Model); } <div id="MoreInfoDiv"> </div> <div align="center" style="margin-bottom: 9px;"> <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span> </div> <div id="ProgressDiv" align="center" style="display: none"> <br /> <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..." /> </div> @section JavaScript { <script type="text/javascript"> $(document).ready(function () { $("#moreInfoButton").InfiniteScroll({ moreInfoDiv: '#MoreInfoDiv', progressDiv: '#ProgressDiv', loadInfoUrl: '@loadInfoUrl', loginUrl: '/login', errorHandler: function () { alert('خطایی رخ داده است'); }, completeHandler: function () { // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد }, noMoreInfoHandler: function () { alert('اطلاعات بیشتری یافت نشد'); } }); }); </script> }
1) مسیر دقیق اکشن متد PagedIndex توسط متد Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده میکنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایلهای آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک میکند، توسط افزونه جیکوئری ما نمایان شده و در پایان کار مجددا مخفی میگردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام میدهد.
و کدهای افزونه اسکرول نامحدود
// <![CDATA[ (function ($) { $.fn.InfiniteScroll = function (options) { var defaults = { moreInfoDiv: '#MoreInfoDiv', progressDiv: '#Progress', loadInfoUrl: '/', loginUrl: '/login', errorHandler: null, completeHandler: null, noMoreInfoHandler: null }; var options = $.extend(defaults, options); var showProgress = function () { $(options.progressDiv).css("display", "block"); } var hideProgress = function () { $(options.progressDiv).css("display", "none"); } return this.each(function () { var moreInfoButton = $(this); var page = 1; $(moreInfoButton).click(function (event) { showProgress(); $.ajax({ type: "POST", url: options.loadInfoUrl, data: JSON.stringify({ page: page }), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = options.loginUrl; } else if (status === 'error' || !data) { if (options.errorHandler) options.errorHandler(this); } else { if (data == "no-more-info") { if (options.noMoreInfoHandler) options.noMoreInfoHandler(this); } else { var $boxes = $(data); $(options.moreInfoDiv).append($boxes); } page++; } hideProgress(); if (options.completeHandler) options.completeHandler(this); } }); }); }); }; })(jQuery); // ]]>
هربار که کاربر بر روی دکمه بیشتر کلیک میکند، progress div ظاهر میگردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال میگردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه میکند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی میکند.
دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip