WHILE (@pNumber) <> '0' BEGIN SET @number = RIGHT(@pNumber, 3) + 0 INSERT INTO @MyNumbers SELECT @Number / 100 * 100, CASE WHEN nbr BETWEEN 10 AND 19 THEN nbr ELSE nbr / 10 * 10 END, CASE WHEN nbr BETWEEN 10 AND 19 THEN 0 ELSE nbr % 10 END FROM (SELECT @Number % 100)S(nbr); IF LEN(@pNumber) > 2 SET @pNumber = LEFT(@pNumber, LEN(@pNumber) -3) ELSE SET @pNumber = '0' END
برای مثال در برنامههای ASP.NET Core، یک چنین فرمی:
<form asp-controller="Manage" asp-action="ChangePassword" method="post"> <!-- Form details --> </form>
<form method="post" action="/Manage/ChangePassword"> <!-- Form details --> <input name="__RequestVerificationToken" type="hidden" value="CfDJ8NrAkSldwD9CpLR...LongValueHere!" /> </form>
تولید خودکار کوکیهای Anti-forgery tokens برای برنامههای Angular
در سمت Angular، مطابق مستندات رسمی آن (^ و ^)، اگر کوکی تولید شدهی توسط برنامه، دارای نام مشخص «XSRF-TOKEN» باشد، کتابخانهی HTTP آن به صورت خودکار مقدار آنرا استخراج کرده و به درخواست بعدی ارسالی آن اضافه میکند. بنابراین در سمت ASP.NET Core تنها کافی است کوکی مخصوص فوق را تولید کرده و به Response اضافه کنیم. مابقی آن توسط Angular به صورت خودکار مدیریت میشود.
میتوان اینکار را مستقیما داخل متد Configure کلاس آغازین برنامه انجام داد و یا بهتر است جهت حجیم نشدن این فایل و مدیریت مجزای این مسئولیت، یک میانافزار مخصوص آنرا تهیه کرد:
using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace AngularTemplateDrivenFormsLab.Utils { public class AntiforgeryTokenMiddleware { private readonly RequestDelegate _next; private readonly IAntiforgery _antiforgery; public AntiforgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery) { _next = next; _antiforgery = antiforgery; } public Task Invoke(HttpContext context) { var path = context.Request.Path.Value; if (path != null && !path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) { var tokens = _antiforgery.GetAndStoreTokens(context); context.Response.Cookies.Append( key: "XSRF-TOKEN", value: tokens.RequestToken, options: new CookieOptions { HttpOnly = false // Now JavaScript is able to read the cookie }); } return _next(context); } } public static class AntiforgeryTokenMiddlewareExtensions { public static IApplicationBuilder UseAntiforgeryToken(this IApplicationBuilder builder) { return builder.UseMiddleware<AntiforgeryTokenMiddleware>(); } } }
- در اینجا ابتدا سرویس IAntiforgery به سازندهی کلاس میان افزار تزریق شدهاست. به این ترتیب میتوان به سرویس توکار تولید توکنهای Antiforgery دسترسی یافت. سپس از این سرویس جهت دسترسی به متد GetAndStoreTokens آن برای دریافت محتوای رشتهای نهایی این توکن استفاده میشود.
- اکنون که به این توکن دسترسی پیدا کردهایم، تنها کافی است آنرا با کلید مخصوص XSRF-TOKEN که توسط Angular شناسایی میشود، به مجموعهی کوکیهای Response اضافه کنیم.
- علت تنظیم مقدار خاصیت HttpOnly به false، این است که کدهای جاوا اسکریپتی Angular بتوانند به مقدار این کوکی دسترسی پیدا کنند.
پس از تدارک این مقدمات، کافی است متد الحاقی کمکی UseAntiforgeryToken فوق را به نحو ذیل به متد Configure کلاس آغازین برنامه اضافه کنیم؛ تا کار نصب میان افزار AntiforgeryTokenMiddleware، تکمیل شود:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseAntiforgeryToken();
پردازش خودکار درخواستهای ارسالی از طرف Angular
تا اینجا برنامهی سمت سرور ما کوکیهای مخصوص Angular را با کلیدی که توسط آن شناسایی میشود، تولید کردهاست. در پاسخ، Angular این کوکی را در هدر مخصوصی به نام «X-XSRF-TOKEN» به سمت سرور ارسال میکند (ابتدای آن یک X اضافهتر دارد).
به همین جهت به متد ConfigureServices کلاس آغازین برنامه مراجعه کرده و این هدر مخصوص را معرفی میکنیم تا دقیقا مشخص گردد، این توکن از چه قسمتی باید جهت پردازش استخراج شود:
public void ConfigureServices(IServiceCollection services) { services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN"); services.AddMvc(); }
یک نکته: اگر میخواهید این کلیدهای هدر پیش فرض Angular را تغییر دهید، باید یک CookieXSRFStrategy سفارشی را برای آن تهیه کنید.
اعتبارسنجی خودکار Anti-forgery tokens در برنامههای ASP.NET Core
ارسال کوکی اطلاعات Anti-forgery tokens و سپس دریافت آن توسط برنامه، تنها یک قسمت از کار است. قسمت بعدی، بررسی معتبر بودن آنها در سمت سرور است. روش متداول انجام اینکار، افزودن ویژگی [ValidateAntiForgeryToken] به هر اکشن متد مزین به [HttpPost] است:
[HttpPost] [ValidateAntiForgeryToken] public IActionResult ChangePassword() { // ... return Json(…); }
public void ConfigureServices(IServiceCollection services) { services.AddAntiforgery(x => x.HeaderName = "X-XSRF-TOKEN"); services.AddMvc(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }); }
یک نکته: در این حالت بررسی سراسری، اگر در موارد خاصی نیاز به این اعتبارسنجی خودکار نبود، میتوان از ویژگی [IgnoreAntiforgeryToken] استفاده کرد.
آزمایش برنامه
برای آزمایش مواردی را که تا کنون بررسی کردیم، همان مثال «فرمهای مبتنی بر قالبها در Angular - قسمت پنجم - ارسال اطلاعات به سرور» را بر اساس نکات متدهای ConfigureServices و Configure مطلب جاری تکمیل میکنیم. سپس برنامه را اجرا میکنیم:
همانطور که ملاحظه میکنید، در اولین بار درخواست برنامه، کوکی مخصوص Angular تولید شدهاست.
در ادامه اگر فرم را تکمیل کرده و ارسال کنیم، وجود هدر ارسالی از طرف Angular مشخص است و همچنین خروجی هم با موفقیت دریافت شدهاست:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-template-driven-forms-lab-09.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس به ریشهی پروژه وارد شده و دو پنجرهی کنسول مجزا را باز کنید. در اولی دستورات
>npm install >ng build --watch
>dotnet restore >dotnet watch run
using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { store.DatabaseCommands.PutAttachment(key: "file/1", etag: null, data: System.IO.File.OpenRead(@"D:\Prog\packages.config"), metadata: new RavenJObject { { "Description", "توضیحات فایل" } }); var question = new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test....", FileId = "file/1" }; session.Store(question); session.SaveChanges(); } }
RavenFS یک فایل سیستم مجازی توزیع شدهاست و برای فایلهای بزرگ چند گیگابایتی به طور بهینهای طراحی گردیدهاست تا کارآیی بانک اطلاعاتی را بالا ببرد و وجود فایلهای تکراری، از بین برود. این سیستم جدید شامل سیستم پیش فرض ایندکس گذاری میباشد و به شما این اجازه را میدهد تا بر روی متادیتاهای یک فایل از قبیل حجم، تاریخ آخرین نگارش و حتی متادیتاهای اختصاصی که شما در حین ذخیره سازی به آن اضافه میکنید، به جستوجو بپردازد. این سیستم جدید همچنین این امکان را به شما میدهد تا این اطلاعات را بین Nodeها، با کمترین میزان انتقالات جابجا کنید و دسترسی سریعتری را بین نودهای مختلف داشته باشید.
برای ذخیره سازی یک فایل ابتدا باید یک FileStore را همانند آنچه که برای DocumentStore داشتید تعریف کنید. Url که شامل همان رشته اتصالی بوده و DefaultFileSystem هم همانند DefaultDatabase که نام دیتابیس در آن ذکر میشد، در اینجا نام فایل سیستم ذکر میگردد:
var fileStore = new FilesStore() { Url = "http://localhost:8080", DefaultFileSystem = "SampleFs" }; fileStore.Initialize();
بعد از آن باید از طریق Store جدید یک سشن ایجاد شود و فایل مورد نظر را در قالب یک استریم بخوانیم:
var session = fileStore.OpenAsyncSession(); var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv");
توجه داشته باشید که برای کار با فایل سیستم، همه متدهای session به صورت غیرهمزمان بوده و متد همزمانی وجود ندارد. سپس در مرحله بعد میخواهم متادیتاهای شخصی نیز به آن اضافه کنیم:
var metadata = new RavenJObject { {"User", "users/1345"}, {"Director","Francis Ford Coppola" }, {"Year","1979" } };
با استفاده از شیء RavenObject میتوانیم در قالب کلید و مقدار، مقادیر خود را ذخیره کنیم و بعد از آن همه موارد بالا که شامل فایل هدر، استریم و متادیتای اختصاصی است را رجیستر کنیم. اگر هم چندین فایل داریم میتوانید آنها را هم در همینجا رجیستر کنید:
session.RegisterUpload("mkv/sample.mkv", stream, metadata);
در مرحله بعدی تغییرات را تایید و عملیات آپلود آغاز میگردد:
await session.SaveChangesAsync();
همانطور که میبینید تمامی متدهای کاربردی این سشن به طور غیرهمزمان طراحی شدهاند.
کلیه عملیاتی که در بالا انجام شد:
var fileStore = new FilesStore() { Url = "http://localhost:8080", DefaultFileSystem = "SampleFs" }; fileStore.Initialize(); var session = fileStore.OpenAsyncSession(); var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv"); var metadata = new RavenJObject { {"User", "users/1345"}, {"Director","Francis Ford Coppola" }, {"Year","1979" } }; session.RegisterUpload("Mkv/sample.mkv", stream, metadata); await session.SaveChangesAsync();
حالا اگر به نسخه سرور ravenDb مراجعه کنید میبینید که فایل طبق فایل هدر داده شده قرار گرفته است و اطلاعات مربوط به آن ذخیره شده است:
{ "User": "users/1345", "Country": "Iran", "City": "Kashan", "Raven-Synchronization-History": [ { "Version": 4, "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4" }, { "Version": 5, "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4" } ], "Raven-Synchronization-Version": "6", "Raven-Synchronization-Source": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4" }
برای خواندن هم به شیوه زیر عمل میکنیم:
از طریق Store ایجاد شده، یک سشن جدید را باز میکنیم و فایل مورد نظر را از طریق یکی از متادیتاهای تعریف شده بازیابی میکنیم:
var fileStore = new FilesStore() { Url = "http://localhost:8080", DefaultFileSystem = "SampleFs" }; fileStore.Initialize(); var session = fileStore.OpenAsyncSession(); var file = await session.Query() .WhereEquals("Year", "1979") .FirstOrDefaultAsync();
var stream = await session.DownloadAsync("mkv/"+file.Name);
سپس استریم را روی دیسک سخت دخیره یا به هر مکانی که مد نظر است ارسال میکنیم:
var fs = File.Create("D:\\file2.mkv"); stream.CopyTo(fs); fs.Flush(); fs.Close();
await stream.CopyToAsync(fs);
سپس کل کد بازیابی را به شکل زیر مینویسیم:
var fileStore = new FilesStore() { Url = "http://localhost:8080", DefaultFileSystem = "SampleFs" }; fileStore.Initialize(); var session = fileStore.OpenAsyncSession(); var file = await session.Query() .WhereEquals("Year", "1979") .FirstOrDefaultAsync(); var stream = await session.DownloadAsync("mkv/"+file.Name); var fs = File.Create("D:\\file2.mkv"); await stream.CopyToAsync(fs);
آموزش LINQ بخش پنجم
• عبارت group
• عبارت orderby
• کلمات کلیدی ascending و descending
• کلمه کلیدی by
بررسی عبارت group و کلمهی کلیدی by
عبارت group یک توالی یک بعدی (flat sequence) یا ساده را از ورودی دریافت و یک توالی گروه بندی شده را تولید میکند.
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Lard", Calories=500}, new Ingredient{Name = "Butter", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=100}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Oats", Calories=50} }; IEnumerable<IGrouping<int, Ingredient>> query = from i in ingredients group i by i.Calories; foreach (IGrouping<int, Ingredient> group in query) { Console.WriteLine($"Ingredients with {group.Key} calories"); foreach (Ingredient ingredient in group) { Console.WriteLine($"- { ingredient.Name}"); } }
Ingredients with 500 calories - Sugar - Lard - Butter Ingredients with 100 calories - Egg - Milk Ingredients with 50 calories - Flour - Oats
برای آشنایی بیشتر با اینترفیس IGrouping اینجا را مطالعه کنید.
عبارت orderby و کلمات کلیدی ascending و descending
عبارت orderby برای تولید یک توالی مرتب شدهی بصورت صعودی (ascending) و یا نزولی (descending) مورد استفاده قرار میگیرد.
مثال: توالی مثال قبل را در نظر بگیرد:
IOrderedEnumerable<Ingredient> sortedByNameQuery = from i in ingredients orderby i.Name select i; foreach (var ingredient in sortedByNameQuery) { Console.WriteLine(ingredient.Name); }
Butter Egg Flour Lard Milk Oats Sugar
IOrderedEnumerable<Ingredient> sortedByNameQuery = from i in ingredients orderby i.Name descending select i;
Sugar Oats Milk Lard Flour Egg Butter
Ingredient[] ingredients = { new Ingredient{Name = "Sugar", Calories=500}, new Ingredient{Name = "Lard", Calories=500}, new Ingredient{Name = "Butter", Calories=500}, new Ingredient{Name = "Egg", Calories=100}, new Ingredient{Name = "Milk", Calories=100}, new Ingredient{Name = "Flour", Calories=50}, new Ingredient{Name = "Oats", Calories=50} }; IEnumerable<IGrouping<int, Ingredient>> query = from i in ingredients group i by i.Calories into calorieGroup orderby calorieGroup.Key select calorieGroup; foreach (IGrouping<int, Ingredient> group in query) { Console.WriteLine($"Ingredients with {group.Key} calories"); foreach (Ingredient ingredient in group) { Console.WriteLine($"- { ingredient.Name}"); } }
Ingredients with 50 calories - Flour - Oats Ingredients with 100 calories - Egg - Milk Ingredients with 500 calories - Sugar - Lard - Butter
در این سری آموزشی با دو روش نوشتن پرس و جو، در LINQ آشنا شدیم:
1- Fluent Style (استفاده از متدهای الحاقی برای انجام عملیاتهای مختلف بر روی توالی)
2- Expression Style (استفاده از کلمات کلیدی (key word) برای انجام عملیاتهای مختلف بر روی توالی)
هر یک از روشهای فوق مزایایی دارند که با توجه به شرایطی که با آن روبرو هستیم از آنها استفاده میکنیم:
var q1 = ingredients.Where(x => x.Calories > 100); var q2 = from i in ingredients where i.Calories > 100 select i;
• پرس و جویی که قرار است نوشته شود، از نظر عملیات بر روی توالی ساده باشد: در این حالت روش استفاده شده تفاوتی نمیکند و بستگی به روش برگزیده و مورد علاقهی برنامه نویس و یا تیم برنامه نویسی دارد. مثلا زمانیکه تنها عملگرهای Where و orderby بخواهند اجرا شود.
• پرس و جوهایی که با متغیرهای range مختلفی رو برو هستند: در این حالت استفاده از عبارتهای پرس و جو راحت از روش عملگرهای پرس و جو میباشد.
لیستی از عملگرهای جستجو که در روش عبارتهای جستجو معادل آنها مهیا شده است :
• GroupBy
• GroupJoin
• Join
• OrderBy
• OrderByDescending
• Select
• SelectMany
• ThenBy
• ThenByDescending
• Where
در بسیاری از پرس و جوها میتوانیم هر دو روش را با هم ترکیب کنیم که نمونهای از آن، در جلسهی چهارم در زمان استفادهی از متد DefaultIfEmpty استفاده شد. درمثال زیر استفاده از عملگر count، با استفاده از روش اول، به همراه عبارت پرس و جوی تولید شدهی با روش دوم، نمایش داده شده است:
int mixedQuery = (from i in ingredients where i.Calories > 100 select i).Count();
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
پیشتر با نحوهی فعال سازی صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid آشنا شدیم. اما ... شاید علاقمند نباشید که اصلا از این صفحات استفاده کنید. شاید به نظر شما با کلیک بر روی دکمهی + افزودن یک رکورد جدید، بهتر باشد داخل خود گرید، یک سطر خالی جدید باز شده تا بتوان آنرا پر کرد. شاید این نحو کار کردن با گرید، از دید عدهای طبیعیتر باشد نسبت به حالت نمایش صفحات popup افزودن و یا ویرایش رکوردها. در ادامه این مورد را بررسی خواهیم کرد.
فعال سازی افزودن، ویرایش و حذف Inline
فعال سازی ویرایش و حذف Inline را پیشتر نیز بررسی کرده بودیم. تنها کافی است یک ستون جدید را با 'formatter: 'actions تعریف کنیم. به صورت خودکار، دکمهی ویرایش، حذف، ذخیره سازی و لغو Inline ظاهر میشوند و همچنین بدون نیاز به کدنویسی بیشتری کار میکنند.
اما در کدهای ذیل اندکی این ستون را سفارشی سازی کردهایم. در قسمت formatter آن، دکمههای edit و delete یک سطر جدید توکار اضافه شده را حذف کردهایم. زیرا در این حالت خاص، وجود این دکمهها ضروری نیستند. بهتر است در این حالت دکمههای save و cancel ظاهر شوند:
$('#list').jqGrid({ caption: "آزمایش نهم", // .... colModel: [ { name: 'myac', width: 80, fixed: true, sortable: false, resize: false, //formatter: 'actions', formatter: function (cellvalue, options, rowObject) { if (cellvalue === undefined && options.rowId === "_empty") { // در حالت نمایش ردیف توکار جدید دکمههای ویرایش و حذف معنی ندارند options.colModel.formatoptions.editbutton = false; options.colModel.formatoptions.delbutton = false; } return $.fn.fmatter.actions(cellvalue, options, rowObject); }, formatoptions: { keys: true, afterSave: function (rowid, response) { }, delbutton: true, delOptions: { url: "@Url.Action("DeleteUser", "Home")" } } } ], //... }).navGrid( '#pager', //... ) .jqGrid('gridResize', { minWidth: 400, minHeight: 150 }) .jqGrid('inlineNav', '#pager', { edit: true, add: true, save: true, cancel: true, edittext: "ویرایش", addtext: "جدید", savetext: "ذخیره", canceltext: "لغو", addParams: { // اگر میخواهید ردیفهای جدید در ابتدا ظاهر شوند، این سطر را حذف کنید position: "last", //ردیفهای جدید در آخر ظاهر میشوند rowID: '_empty', useDefValues: true, addRowParams: getInlineNavParams(true) }, editParams: getInlineNavParams(false) });
در اینجا 4 دکمهی ویرایش، جدید، ذخیره و لغو، در نوار pager پایین گرید ظاهر خواهند شد (سمت چپ؛ سمت راست همان دکمههای نمایش فرمهای پویا هستند).
سپس باید دو قسمت مهم addParams و editParams آنرا مقدار دهی کرد.
در قسمت addParams، مشخص میکنیم که ID ردیف اضافه شده، مساوی کلمهی _empty باشد. اگر به کدهای formatter ستون action دقت کنید، از این ID برای تشخیص افزوده شدن یک ردیف جدید استفاده شدهاست.
position در اینجا به معنای محل افزوده شدن یک ردیف خالی است. مقدار پیش فرض آن first است؛ یعنی همیشه در اولین ردیف گرید، این ردیف جدید اضافه میشود. در اینجا به last تنظیم شدهاست تا در پایین گرید و پس از رکوردهای موجود، نمایش داده شود.
useDefValues سبب استفاده از مقادیر پیش فرض تعریف شده در ستونهای گرید در حین افزوده شدن یک ردیف جدید میگردد.
addRowParams و editParams هر دو ساختار تقریبا یکسانی دارند که به نحو ذیل تعریف میشوند:
function getInlineNavParams(isAdd) { return { // استفاده از آدرسهای مختلف برای حالات ویرایش و ثبت اطلاعات جدید url: isAdd ? '@Url.Action("AddUser", "Home")' : '@Url.Action("EditUser","Home")', key: true, restoreAfterError: false, // این مورد سبب میشود تا اعتبارسنجی سمت سرور قابل اعمال شود oneditfunc: function (rowId) { // نمایش دکمههای ذخیره و لغو داخل همان سطر $("#jSaveButton_" + rowId).show(); $("#jCancelButton_" + rowId).show(); }, successfunc: function () { var $self = $(this); setTimeout(function () { $self.trigger("reloadGrid"); // دریافت کلید اصلی ردیف از سرور }, 50); }, errorfunc: function (rowid, response, stat) { if (stat != 'error') // this.Response.StatusCode == 200 return; var result = $.parseJSON(response.responseText); if (result.success === false) { //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن $.jgrid.info_dialog($.jgrid.errors.errcap, '<div class="ui-state-error">' + result.message + '</div>', $.jgrid.edit.bClose, { buttonalign: 'center' }); } } }; }
تنظیم restoreAfterError به false بسیار مهم است. اگر در سمت سرور خطای اعتبارسنجی گزارش شود و restoreAfterError مساوی true باشد (مقدار پیش فرض)، کاربر مجبور خواهد شد اطلاعات را دوباره وارد کند.
در روال رویدادگران oneditfunc دکمهی save و cancel ردیف را که مخفی هستند، ظاهر میکنیم (مکمل formatter ستون action است).
در قسمت successfunc، پس از پایان موفقیت آمیز کار، متد reloadGrid را فراخوانی میکنیم. اینکار سبب میشود تا Id واقعی رکورد، از سمت سرور دریافت شود. از این Id برای ویرایش و همچنین حذف، استفاده خواهد شد. علت استفاده از setTimeout در اینجا این است که اندکی به DOM فرصت داده شود تا کارش به پایان برسد.
در قسمت errorfunc خطاهای اعتبارسنجی سفارشی سمت سرور را میتوان دریافت و سپس توسط متد توکار info_dialog به کاربر نمایش داد.
یک نکتهی مهم در مورد ارسال خطاهای اعتبارسنجی از سمت سرور در حالت Inline Add
if (_usersInMemoryDataSource.Any( user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase))) { this.Response.StatusCode = 500; //این مورد برای افزودن داخل ردیفهای گرید لازم است return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet); }
مدیریت StatusCodeهای غیر از 200 در حالت کار با فرمهای jqGrid
اگر هر دو حالت Inline Add و فرمهای پویا را فعال کردهاید، بازگشت StatusCode = 500 سبب میشود تا دیگر نتوان خطاهای سفارشی سمت سرور را در بالای فرمها به کاربر نمایش داد و در این حالت تنها یک internal server error را مشاهده خواهند کرد. برای رفع این مشکل فقط کافی است روال رویدادگران errorTextFormat را مدیریت کرد:
$('#list').jqGrid({ caption: "آزمایش نهم", //......... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { //......... errorTextFormat: serverErrorTextFormat }, //add options { //......... errorTextFormat: serverErrorTextFormat }, //delete options { //......... }) .jqGrid('gridResize', { minWidth: 400, minHeight: 150 }) .jqGrid('inlineNav', '#pager', { //......... }); function serverErrorTextFormat (response) { // در حالتیکه وضعیت خروجی از سرور 200 نیست فراخوانی میشود var result = $.parseJSON(response.responseText); if (result.success === false) { return result.message; } return "لطفا ورودیهای وارد شده را بررسی کنید"; }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
jqGrid09.zip
if/else statement
دستور if/else، یک از اصلیترین ساختارهای Control Flow است که تقریباً در تمام زبانهای برنامه نویسی وجود دارد. در Rust، دستور if/else، برای اجرای یک بلوک کد، بر اساس یک شرط معین استفاده میشود. نحو دستور if/else در Rust، به شرح زیر است:
if condition { // اجرای دستور اگر شرط درست باشد } else { // اجرای دستور اگر شراط نادرست باشد } fn main() { let x = 5; if x < 10 { println!("x is less than 10"); } else { println!("x is greater than or equal to 10"); } }
x is less than 10
Loop
دستور حلقه، برای ایجاد یک حلقهی بینهایت در Rust استفاده میشود. دستور حلقه زمانی مفید است که بخواهیم یک بلوک کد را تا زمانیکه یک شرط خاص برآورده شود، تکرار کنیم. در اینجا syntax حلقه در Rust، آمدهاست:
loop { // اجرای کد } fn main() { let mut counter = 0; loop { counter += 1; if counter == 5 { break; } } println!("Counter value: {}", counter); }
Counter value: 5
While loop
حلقه while، یکی دیگر از ساختارهای Control Flow در Rust است که برای تکرار یک بلوک کد، تا زمانیکه یک شرط خاص برآورده شود، استفاده میشود. حلقه while زمانی مفید است که از قبل، تعداد تکرارها را نمیدانیم. در اینجا syntax حلقه while در Rust آمده است:
while condition { // اجرای دستور } fn main() { let mut counter = 0; while counter < 5 { println!("Counter value: {}", counter); counter += 1; } }
Counter value: 0 Counter value: 1 Counter value: 2 Counter value: 3 Counter value: 4
For loop
حلقهی for، یکی دیگر از ساختارهای Control Flow در Rust است که برای تکرار در محدودهای از مقادیر یا مجموعهای از آیتمها استفاده میشود. حلقهی for زمانی مفید است که از قبل تعداد تکرارها را بدانیم. در اینجا syntax حلقه for در Rust آمدهاست:
for item in collection { // اجرای دستور } fn main() { let arr = [1, 2, 3, 4, 5]; for element in arr.iter() { println!("Element: {}", element); } }
Element: 1 Element: 2 Element: 3 Element: 4 Element: 5
در این مقاله با ادیتور VS Code کار میکنیم. بعد از نصب آن، از منوی Terminal، گزینهی New Terminal را کلیک کنید تا پنجرهی PowerShell نمایش داده شود؛ برای سرعت و دقت بیشتر در برنامههای vue.js ای. با دستور زیر vue cli را نصب میکنیم (فقط یک مرتبه و برای برنامههای بعدی vue.jsای، نیازی به اجرای این دستور نداریم):
npm install -g @vue/cli
جهت راه اندازی یک برنامهی پیش فرض Vue.js ای، کافیست دستور زیر را اجرا نماییم تا پکیجهای مورد نیاز، به همراه کانفیگ اولیه (Zero config) برای ما ایجاد شوند:
vue create movie-app
بعد از ایجاد برنامه در vs code، از طریق منوی File، گزینه Open Folder را کلیک کرده و پوشه برنامهای را که ایجاد کردیم، Select Folder میکنیم. ساختار اولیهی برنامهی ایجاد شده، به شکل زیر میباشد:
نیازمندیهای مثال جاری
A) برای گرفتن اطلاعات مورد نمایش در مثال جاری، از سایت omdbapi.com استفاده میکنیم که با دریافت یک api key آن بصورت رایگان، میتوانیم web serviceهای آن را Call نماییم.
B) از vuetify برای ui استفاده میکنیم که بصورت Material Design و دارای کامپوننتهای غنی میباشد؛ ضمن اینکه RTL را هم پشتیبانی میکند.
برای نصب آن در Terminal دستور زیر را اجرا میکنیم:
vue add vuetify
سپس جهت تست و صحت افزوده شدن و کانفیگ درست، با دستور زیر برنامه را اجرا میکنیم:
npm run serve
بعد از اجرای دستور فوق، روی گزینه زیر ctrl+click میکنیم تا نتیجه کار در مرورگر قابل رویت باشد:
نمایش صفحه زیر نشان دهندهی درستی انجام کار تا اینجا است:
نکته: جهت استفاده از امکان RTL کافیست در فایل vuetify.js واقع در پوشهی plugins، تغییرات زیر را انجام دهیم. در مثال جاری بدلیل اینکه اطلاعات انگلیسی میباشند، از نسخه LTR آن استفاده میکنیم؛ هر چند یکسری api فارسی نیز موجود میباشد که میتوان از آنها استفاده نمود.
import Vue from 'vue' import Vuetify from 'vuetify/lib' import 'vuetify/src/stylus/app.styl' Vue.use(Vuetify, { iconfont: 'md', rtl: true })
C) نصب vue-router : جهت انجام routeهای تودرتو ، مپ کردن کامپوننت ها با آدرسی مشخص، کار با پارامتر و HTML5 History API مورد استفاده قرار میگیرد. برای نصب آن، دستور زیر را اجرا میکنیم:
npm install vue-router
برای نوشتن routeهای مورد نیاز، یک فولدر را با نام router، در پوشه src برنامه ایجاد میکنیم و یک فایل جاوا اسکریپتی را در آن با نام index.js، میسازیم (این ساختار برای مدیریت بهتر پروژه میباشد):
درون فایل index.js، محتویات زیر را طبق مستندات آن قرار میدهیم:
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter)
جهت استفاده از این router، نیاز است تا در نمونهی وهله سازی شدهی vue برنامه بکار گرفته شود. فایل main.js را باز کنید و خط زیر را در قسمت بالای برنامه وارد کنید:
import router from './router'
اکنون محتویات فایل main.js بشکل زیر میباشد:
import Vue from 'vue' import './plugins/vuetify' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ render: h => h(App), router }).$mount('#app')
D) نصب axios : برای انجام درخواستهای HTTP و عملیات ایجکس در vue.js ترجیحا بهتر است از axios که یک کتابخانهی محبوب میباشد و کار با آن ساده است، استفاده شود. برای نصب آن، دستور زیر را اجرا میکنیم:
npm install axios
E) نصب vuex : کتابخانهای جهت مدیریت حالت (state management) برای vue.js میباشد و مشابه آن Flux و Redux برای React میباشند. برای نصب، دستور زیر را اجرا میکنیم:
npm install vuex
برای بکارگیری آن یک فولدر را با نام store در پوشهی src برنامه ایجاد میکنیم و یک فایل جاوا اسکریپتی را در آن با نام index.js میسازیم (این ساختار برای مدیریت بهتر پروژه میباشد). درون فایل index.js، محتویات زیر را طبق مستندات آن و ^ قرار میدهیم.
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export const store = new Vuex.Store()
برای استفاده و کانفیگ آن، محتویات فایل main.js را بشکل زیر تغییر دهید:
import Vue from 'vue' import './plugins/vuetify' import App from './App.vue' import router from './router' import {store} from './store' Vue.config.productionTip = false new Vue({ render: h => h(App), store, router }).$mount('#app')
نکته: برای اجرای برنامه و دریافت پکیجهای مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:
npm install
در نرم افزارهای بزرگ و چند کاربره، اتصال به بانک اطلاعاتی کامپیوتر سرور، یکی از نیازهای اساسی برنامه نویسان محسوب میگردد. در این بخش با دو اصطلاح بسیار مهم سروکار داریم.
1. کلاینت (Client): منظور از کلاینت کامپیوتری است که میخواهد به سرور متصل گردد و از SQL کامپیوتر سرور خدماتی را دریافت نماید.
2. سرور (Server): کامپیوتری است که میخواهیم به آن متصل شویم و دادهها را بصورت متمرکز بر روی آن ذخیره و بازیابی نماییم.
به دو روش میتوان به سرور متصل شد:
1. Windows Authentication
در این روش جهت اتصال به بانک اطلاعاتی، کامپیوتر مبدا یا Client باید عضو شبکه ای باشد کهServer در آن وجود دارد. در واقع برای شبکه هایی استفاده میشوند که دارای Domain می باشند وClient به عنوان یک کاربر شناخته شده در سرور تعریف شده است.
2. SQL Authentication
در این روش کلاینت به عنوان یک کاربر یا Login در SQL تعریف شده است و دارای نام کاربری و رمز عبور میباشد.
جهت اتصال از راه دور به یک سرور دارای SQL، باید تنظیمات زیر را برای کامپیوتر سرور انجام دهیم:
1. به SQL Server کامپیوتر سرور متصل شوید.
2. در پنجره Object Explorer بر روی نام سرور (اولین آیتم موجود در لیست) کلیک راست کنید و گزینهProperties را انتخاب نمایید.
3. در پنجره ظاهر شده (Server Properties) و در قسمت Select a page (سمت چپ پنجره) بر رویSecurity کلیک کنید.
4. در سمت راست پنجره گزینه SQL Server and Windows Authentication mode را انتخاب کنید.
5. دکمه OK را انتخاب کنید. پنجره پیغامی مبنی بر Restart کردن سرور نمایش داده میشود. این پنجره را تایید کنید.
6. مجددا بر روی نام سرور کلیک راست کنید و گزینه Restart را انتخاب نموده و در پیغام ظاهر شده Yesرا انتخاب نمایید.
تا به اینجا سرور آماده پذیرش اتصال از راه دور بصورت SQL Authentication می باشد. حال نوبت به تعریف یک Login می باشد تا توسط این Login بتوانید به سرور از راه دور متصل شوید. مراحل زیر را برای تعریفLogin دنبال کنید:
1. در پنجره Object Explorer به مسیر Security > Logins بروید.
2. بر روی پوشه Logins کلیک راست نموده و گزینه New Login… را انتخاب نمایید.
3. در پنجره ظاهر شده در بخش Login name نامی را به کاربر اختصاص دهید. (به عنوان مثال user1)
4. گزینه SQL Server authentication را انتخاب نموده و در بخش Password و Confirm password رمز عبوری را به این کاربر اختصاص دهید. (به عنوان مثال abc123)
5. گزینه Enforce password policy را از حالت انتخاب خارج کنید تا رمز عبور را از قید سیاستهای رمزگذاری ویندوز خارج کنید.
6. در قسمت Select a page (سمت چپ پنجره) بر روی Server Roles کلیک کنید.
7. در سمت راست پنجره گزینه sysadmin یا هر نوع دسترسی دیگری را که مایل هستید انتخاب نمایید.
توجه: با انتخاب sysadmin کاربر ایجاد شده به کل سرور و بانکهای اطلاعاتی دسترسی کامل یاAdmin دارد. اگر نمیخواهید کاربر چنین دسترسی داشته باشد، در بخش فوق فقط گزینه public انتخاب شده باشد.
8. در قسمت Select a page (سمت چپ پنجره) بر روی User Mapping کلیک کنید. در این بخش نحوه دسترسی کاربر را به بانکهای اطلاعاتی موجود، مشخص میکنیم.
9. در سمت راست پنجره و در بخش Users mapped to this login یک یا چند بانک اطلاعاتی را که میخواهید توسط این Login قابل دسترسی باشند را انتخاب نمایید.
10. پس از انتخاب هر بانک اطلاعاتی، در قسمت پایین (Database role membership for:) نوع دسترسی کاربر به آن Database را انتخاب کنید. در اینجا من db_owner را انتخاب میکنم تا کاربر دسترسی کامل به بانک اطلاعاتی انتخاب شده را داشته باشد.
11. دکمه OK را انتخاب کنید تا Login مورد نظر ساخته شود.
حالا میتوانید از راه دور و حتی از روی خود سرور با کاربر ایجاد شده به سرور متصل شوید. برای این منظور SQL را Disconnect نمایید و یا یکبار SQL Server Management Studio (SSMS) را ببندید و دوباره اجرا نمایید. در پنجره Connect to Server اطلاعات زیر را وارد نمایید:
Server name :نام یا IP سرور (به عنوان مثال 192.168.0.1)
Authentication: انتخاب گزینه SQL Server Authentication
Login: طبق مثال user1
Password: طبق مثال abc123
پس از ورود با مشخصات فوق فقط میتوانید به بانک اطلاعاتی دسترسی باشید که در قسمت User Mapping انتخاب کرده بودید. اگر sysadmin را انتخاب کرده باشید به تمامی بانکهای اطلاعاتی موجود دسترسی دارید.
برخی مشکلات اتصال از راه دور
ممکن است در زمان اتصال از راه دور با مشکل عدم امکان اتصال به سرور مواجه شوید. برای این منظور و اطمینان از صحت تنظیمات سرور، موارد زیر را در سرور بررسی نمایید تا بدرستی تنظیم شده باشند:
1. به مسیر Start > All Programs > Microsoft SQL Server 2008/2005 > Configuration Tools > SQL Server Configuration Manager مراجعه کنید و موارد زیر را بررسی نمایید:
1.1. بر روی SQL Server Services کلیک کنید و در سمت راست پنجره بررسی کنید که ستون Stateمربوط به SQL Server Browser و SQL Server در وضعیت Running باشد.
1.2. بر روی آیتمهای زیر مجموعه SQL Server Network Configuration کلیک کنید و در سمت راست پنجره بررسی کنید که آیتم های Shared Memory، Named Pipes و TCP/IP در وضعیت Enabled باشند.
1.3. بر روی آیتم SQL Native Client Configuration > Client Protocols کلیک کنید و در سمت راست پنجره بررسی کنید که آیتم های Shared Memory، Named Pipes و TCP/IP در وضعیت Enabled باشند.
2. بررسی کنید که فایروال سیستم سرور غیر فعال باشد و یا SQL Server به برنامه های Trust فایروال اضافه شده باشد.
تقویم
در دروس گذشته اطلاعات را از متدی به نام GetPerson دریافت میکردیم که اطلاعات آن به شرح زیر است:
public static Person GetPerson() { return new Person() { Name = "Leo", Gender = true, ImageName = "man.jpg", Country = new Country() { Id = 3, Name = "Angola" }, FieldOfWork = new FieldOfWork[] { test.FieldOfWork.Actor, test.FieldOfWork.Producer }, Date = DateTime.Now.AddMonths(-3) }; }
Calendar DisplayDate="{Binding Date}" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" Margin="10">
<Calendar DisplayDate="{Binding Date}" SelectedDate="{Binding Date}" Grid.Row="4" Grid.Column="1" HorizontalAlignment="Left" Margin="10">
ادامه مفاهیم بایندینگ
در قسمت پنجم، دیدیم که چطور میتوانیم با استفاده از متد OnPropertyName، برنامه را از تغییراتی که در سطح مدل میگذرد، آگاه کنیم و این تغییرات جدید را دریافت کرده و اطلاعات نمایش داده شده را به روز کنیم. در اینجا قصد داریم خلاف اینکار را با استفاده از همان متد انجام دهیم. یعنی مدل را از تغییراتی که در سطح UI میگذرد، آگاه کنیم.
این مثال را روی خصوصیت Name مدل اجرا میکنیم:
در Xaml Editor تگTextBox مربوط به نام شخص را به شکل زیر تغییر میدهیم:
<TextBox Grid.Row="0" Grid.Column="1" Name="Txtname" Text="{Binding Path=Name,Mode=TwoWay}" HorizontalAlignment="Left" Margin="5" Width="200" ></TextBox>
تغییری که در این حالت رخ داده است، افزودن ویژگی به نام Mode است که روی گزینه TwoWay تنظیم شده است. در قسمتهای قبلی تمامی بایندینگها به طور پیش فرض روی حالت یک طرفه OneWay قرار داشتند، ولی در اینجا ما بایندینگ را دو طرفه اعمال کردهایم. حال به همین سادگی هر تغییری که در این TextBox رخ دهد به مدل هم اعمال خواهد شد.
حال برای تست این مورد، عنصر زیر را در کنار نام شخص به صفحه اضافه میکنیم. یک برچسب متنی که به خاصیت Name متصل است و از تغییراتی که در سطح مدل داده میشود، آگاه است:
<TextBlock Grid.Column="1" Text="{Binding Path=Name}" Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="210,10,0,13" RenderTransformOrigin="0.555,1.283" ></TextBlock>
از دیگر مقادیر Mode میتوان به جدول زیر اشاره کرد:
OneWayToSource | در این حالت، مدل از تغییرات سطح UI آگاه میشود ولی بقیه کنترلها یا المانها را از تغییرات خود آگاه نمیکند. |
OneTime | در این حالت تنها یکبار مدل دادههای خود را کنترل کرده (همان پر کردن اولیه دادهها) و دیگر هیچ نوع تغییراتی را رصد نمیکند. |
دانلود مثال
public static class ConcurrentDictionaryExtensions { public static TValue GetOrAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.GetOrAdd(key, (k) => new Lazy<TValue>(() => valueFactory(k)) ).Value; } public static TValue AddOrUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory ) { return @this.AddOrUpdate(key, (k) => new Lazy<TValue>(() => addValueFactory(k)), (k, currentValue) => new Lazy<TValue>( () => updateValueFactory(k, currentValue.Value) ) ).Value; } public static bool TryGetValue<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); var result = @this.TryGetValue(key, out Lazy<TValue> v); if (result) value = v.Value; return result; } // this overload may not make sense to use when you want to avoid // the construction of the value when it isn't needed public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, TValue value ) { return @this.TryAdd(key, new Lazy<TValue>(() => value)); } public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.TryAdd(key, new Lazy<TValue>(() => valueFactory(key)) ); } public static bool TryRemove<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); if (@this.TryRemove(key, out Lazy<TValue> v)) { value = v.Value; return true; } return false; } public static bool TryUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue, TValue> updateValueFactory ) { if (!@this.TryGetValue(key, out Lazy<TValue> existingValue)) return false; return @this.TryUpdate(key, new Lazy<TValue>( () => updateValueFactory(key, existingValue.Value) ), existingValue ); } }