ASP.NET MVC #13
مثال 1: نام تمام کاربران را با قالب 'Surname, Firstname' نمایش دهید.
var members = context.Members .Select(member => new { Name = member.Surname + ", " + member.FirstName }) .ToList();
با این خروجی:
مثال 2: تمام امکاناتی را که با Tennis شروع میشوند، لیست کنید.
این گزارش به همراه تمام ستونهای جدول است.
var facilities = context.Facilities .Where(facility => facility.Name.StartsWith("Tennis")) .ToList();
با این خروجی:
مثال 3: تمام امکاناتی را که با tennis شروع میشوند، لیست کنید. این جستجو باید غیرحساس به بزرگی و کوچکی حروف باشد.
این گزارش به همراه تمام ستونهای جدول است.
نیازی به انجام مجزای این تمرین نیست؛ چون پاسخ آن همان پاسخ مثال 2 است. Collation پیشفرض در SQL Server، غیرحساس به بزرگی و کوچکی حروف است. بنابراین چه tennis را جستجو کنیم و یا TeNnis را، تفاوتی نمیکند.
مثال 4: شماره تلفنهای دارای پرانتز را لیست کنید.
این گزارش باید به همراه ستونهای memid, telephone باشد.
روش اول: در اینجا دوبار از متد Contains استفاده شدهاست:
var members = context.Members .Select(member => new { member.MemId, member.Telephone }) .Where(member => member.Telephone.Contains("(") && member.Telephone.Contains(")")) .ToList();
روش دوم: اگر میخواهیم کنترل بیشتری را بر روی خروجی نهایی LIKE تولیدی داشته باشیم، میتوان از متد سفارشی استاندارد EF.Functions.Like استفاده کرد که از حروف wild cards نیز پشتیبانی میکند:
members = context.Members .Select(member => new { member.MemId, member.Telephone }) .Where(member => EF.Functions.Like(member.Telephone, "%[()]%")) .ToList();
مثال 5: کد پستیها 5 رقمی هستند. گزارشی را تهیه کنید که در آن اگر کدپستی کمتر از 5 رقم بود، ابتدای آن با صفر شروع شود.
هدف اصلی از این مثال، اعمال متد PadLeft(5, '0') به خاصیت member.ZipCode است.
روش اول: EF-Core فعلا قابلیت ترجمهی PadLeft(5, '0') را به معادل SQL آنرا ندارد. به همین جهت مجبور هستیم ابتدا ZipCodeها را به صورت رشتهای بازگشت دهیم که در اینجا استفادهی از Convert.ToString مجاز است.
با این خروجی:
SELECT CONVERT (NVARCHAR (MAX), [m].[ZipCode]) AS [Zip] FROM [Members] AS [m] ORDER BY CONVERT (NVARCHAR (MAX), [m].[ZipCode]);
var members = context.Members .Select(member => new { ZipCode = Convert.ToString(member.ZipCode) }) .OrderBy(m => m.ZipCode) .ToList(); // Now using LINQ to Objects members = members.Select(member => new { ZipCode = member.ZipCode.PadLeft(5, '0') }) .OrderBy(m => m.ZipCode) .ToList();
روش دوم: SQL Server به همراه تابع استانداردی به نام Replicate است که از آن میتوان برای شبیه سازی PadLeft، بدون متوسل شدن به LINQ to Objects، استفاده کرد. اما چون این تابع هنوز به EF-Core معرفی نشدهاست، نیاز است خودمان اینکار را انجام دهیم. در این روش، از متد SqlDbFunctionsExtensions.SqlReplicate استفاده میشود. روش تعریف این نوع متدها را در مطلب «امکان تعریف توابع خاص بانکهای اطلاعاتی در EF Core» پیشتر بررسی کردهایم که برای مثال در اینجا چنین شکلی را پیدا میکند:
namespace EFCorePgExercises.Utils { public static class SqlDbFunctionsExtensions { public static string SqlReplicate(string expression, int count) => throw new InvalidOperationException($"{nameof(SqlReplicate)} method cannot be called from the client side."); private static readonly MethodInfo _sqlReplicateMethodInfo = typeof(SqlDbFunctionsExtensions) .GetRuntimeMethod( nameof(SqlDbFunctionsExtensions.SqlReplicate), new[] { typeof(string), typeof(int) } ); public static void AddCustomSqlFunctions(this ModelBuilder modelBuilder) { modelBuilder.HasDbFunction(_sqlReplicateMethodInfo) .HasTranslation(args => { return SqlFunctionExpression.Create("REPLICATE", args, _sqlReplicateMethodInfo.ReturnType, typeMapping: null); }); } } }
namespace EFCorePgExercises.DataLayer { public class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { // ... modelBuilder.AddCustomSqlFunctions(); // ... } } }
var newMembers = context.Members .Select(member => new { ZipCode = SqlDbFunctionsExtensions.SqlReplicate( "0", 5 - Convert.ToString(member.ZipCode).Length) + member.ZipCode }) .OrderBy(m => m.ZipCode) .ToList();
مثال 6: اولین حرف نام خانوادگی کاربران در کل ردیفهای جدول چندبار تکرار شدهاست؟
این گزارش باید به همراه ستونهای letter, count باشد.
var members = context.Members .Select(member => new { Letter = member.Surname.Substring(0, 1) }) .GroupBy(m => m.Letter) .Select(g => new { Letter = g.Key, Count = g.Count() }) .OrderBy(r => r.Letter) .ToList();
با این خروجی:
مثال 7: حروف '-','(',')', ' ' را از شماره تلفنها حذف کنید.
این گزارش باید به همراه ستونهای memid, telephone باشد.
بانک اطلاعاتی PostgreSQL به همراه تابع استاندارد regexp_replace است و میتوان از آن برای حل یک چنین مسایلی استفاده کرد:
select memid, regexp_replace(telephone, '[^0-9]', '', 'g') as telephone from members order by memid;
var members = context.Members .Select(member => new { member.MemId, Telephone = member.Telephone.Replace("-", "") .Replace("(", "") .Replace(")", "") .Replace(" ", "") }) .OrderBy(r => r.MemId) .ToList();
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.
foreach (string file in Request.Files) { var hpf = Request.Files[file] as HttpPostedFileBase; // todo: save it }
مقدمهای بر RxJS
اگر به پیشنیازهای نصب AngularJS 2.0 در قسمت اول این سری دقت کرده باشید، یکی از موارد آن، RxJS است:
"dependencies": { "rxjs": "5.0.0-beta.2" },
قرار است Observableها به ES 2016 یا نگارش پس از ES 6 اضافه شوند و یکی از پیشنهادات آن هستند. اما هم اکنون AngularJS 2.0 از این امکان، توسط یک کتابخانهی ثالث، به نام reactive extensions یا Rx، استفاده میکند. از RxJS در سرویس HTTP و همچنین مدیریت سیستم رخدادهای AngularJS 2.0 استفاده میشود. Observableها امکانی را فراهم میکنند تا به ازای دریافت هر اطلاعات async از سرور، بتوان توسط رخدادهایی از وقوع آنها مطلع شد.
در نگارش قبلی AngularJS از Promises برای مدیریت اعمال غیرهمزمان استفاده میشد. Observableها تفاوتهای قابل ملاحظهای با Promises دارند:
- یک Promise تنها یک مقدار، یا خطا را بر میگرداند؛ اما یک Observable چندین مقدار را در طول یک بازهی زمانی باز میگرداند.
- برخلاف Promises، میتوان عملیات یک Observable را لغو کرد.
- Observableها از عملگرهایی مانند map، reduce، filter و غیره نیز پشتیبانی میکنند.
البته باید عنوان کرد که هنوز هم میتوان از Promises در صورت تمایل در AngularJS 2.0 نیز استفاده کرد.
تنظیمات اولیهی کار با RxJS در AngularJS 2.0
برای استفاده از RxJS در AngularJS 2.0، مراحلی مانند افزودن مدخل اسکریپت http.dev.js، ثبت پروایدر HTTP و importهای لازم، باید طی شوند که در ادامه آنها را بررسی خواهیم کرد:
الف) سرویس HTTP جزئی از angular2/core نیست. به همین جهت مدخل اسکریپت متناظر با آن، باید به صفحهی اصلی سایت اضافه شود که این مورد، در قسمت اول بررسی پیشنیازهای نصب AngularJS 2.0 صورت گرفتهاست:
<!-- Required for http --> <script src="~/node_modules/angular2/bundles/http.dev.js"></script>
ب) اکنون فایل app.component.ts را گشوده و سرویس HTTP را به آن اضافه میکنیم. با نحوهی ثبت سرویسها در قسمت قبل آشنا شدیم:
import { Component } from 'angular2/core'; import { HTTP_PROVIDERS } from 'angular2/http'; import 'rxjs/Rx'; // Load all features import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ ProductService, HTTP_PROVIDERS ] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
ج) پس از آن نیاز است importهای متناظر نیز به ابتدای ماژول فعلی، جهت شناسایی این سرویس و همچنین امکانات rx.js اضافه شوند.
تعریف 'import 'rxjs/Rx به این شکل، به module loader اعلام میکند که این کتابخانه را بارگذاری کن، اما چیزی را import نکن. هنگامیکه این کتابخانه بارگذاری میشود، کدهای جاوا اسکریپتی آن اجرا شده و سبب میشوند که عملگرهای ویژهی Observable آن مانند map و filter نیز در دسترس برنامه قرار گیرند.
ساخت یک سرویس سمت سرور بازگشت لیست محصولات به صورت JSON
چون در ادامه میخواهیم لیست محصولات را از سرور دریافت کنیم، برنامهی ASP.NET MVC فعلی را اندکی تغییر میدهیم تا این لیست را به صورت JSON بازگشت دهد.
بنابراین ابتدا کلاس مدل محصولات را به نحو ذیل به پوشهی Models اضافه کرده:
namespace MVC5Angular2.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public string ProductCode { set; get; } public string ReleaseDate { set; get; } public decimal Price { set; get; } public string Description { set; get; } public double StarRating { set; get; } public string ImageUrl { set; get; } } }
using System.Collections.Generic; using System.Text; using System.Web.Mvc; using MVC5Angular2.Models; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; namespace MVC5Angular2.Controllers { public class HomeController : Controller { // GET: Home public ActionResult Index() { return View(); } public ActionResult Products() { var products = new List<Product> { new Product { ProductId= 2, ProductName= "Garden Cart", ProductCode= "GDN-0023", ReleaseDate= "March 18, 2016", Description= "15 gallon capacity rolling garden cart", Price= (decimal) 32.99, StarRating= 4.2, ImageUrl= "app/assets/images/garden_cart.png" }, new Product { ProductId= 5, ProductName= "Hammer", ProductCode= "TBX-0048", ReleaseDate= "May 21, 2016", Description= "Curved claw steel hammer", Price= (decimal) 8.9, StarRating= 4.8, ImageUrl= "app/assets/images/rejon_Hammer.png" } }; return new ContentResult { Content = JsonConvert.SerializeObject(products, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
برای مطالعهی بیشتر:
«استفاده از JSON.NET در ASP.NET MVC»
«تنظیمات و نکات کاربردی کتابخانهی JSON.NET»
به این ترتیب، آدرس http://localhost:2222/home/products، خروجی JSON سرویس لیست محصولات را در مثال جاری، ارائه میدهد.
ارسال یک درخواست HTTP به سرور توسط AngularJS 2.0
اکنون پس از تنظیمات ثبت و معرفی سرویس HTTP و همچنین برپایی یک سرویس سمت سرور ارائهی لیست محصولات، میخواهیم سرویس ProductService را که در قسمت قبل ایجاد کردیم (فایل product.service.ts)، جهت دریافت لیست محصولات از سمت سرور، تغییر دهیم:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; import { Http, Response } from 'angular2/http'; import { Observable } from 'rxjs/Observable'; @Injectable() export class ProductService { private _productUrl = '/home/products'; constructor(private _http: Http) { } getProducts(): Observable<IProduct[]> { return this._http.get(this._productUrl) .map((response: Response) => <IProduct[]>response.json()) .do(data => console.log("All: " + JSON.stringify(data))) .catch(this.handleError); } private handleError(error: Response) { console.error(error); return Observable.throw(error.json().error || 'Server error'); } }
در سازندهی کلاس ProductService، کار تزریق وابستگی سرویس Http انجام شدهاست. به این ترتیب با استفاده از متغیر خصوصی http_، میتوان در کلاس جاری به امکانات این سرویس دسترسی یافت (همان «تزریق سرویسها به کامپوننتها» در قسمت قبل).
سپس متد get آن، یک درخواست HTTP از نوع GET را به آدرس مشخص شدهی در متغیر productUrl_ ارسال میکند (یا همان سرویس سمت سرور برنامه).
سرویس Http و همچنین شیء Response آن در ماژولهای Http و Response قرار دارند که در ابتدای صفحه import شدهاند.
متد http get یک Observable را بازگشت میدهد که در نهایت خروجی این متد نیز به همان <[]Observable<IProduct، تنظیم شدهاست. Observable یک شیء جنریک است و در اینجا نوع آن، آرایهای از محصولات درنظر گرفته شدهاست.
اکنون که امضای این متد تغییر یافته است (پیش از این صرفا یک آرایهی ساده از محصولات بود)، استفاده کننده (در کلاس ProductListComponent) باید به تغییرات آن از طریق متد subscribe گوش فرا دهد.
فعلا در کلاس جاری، پس از پایان کار دریافت اطلاعات از سرور، اطلاعات نهایی در متد map در دسترس قرار میگیرد (که یکی از عملگرهای RxJs است). کار متد map، اصطلاحا projection است. این متد، هر عضو دریافتی از خروجی سرور را به فرمتی جدید نگاشت میکند.
هر درخواست HTTP، در اصل یک عملیات async است. یعنی در اینجا توالی که در اختیار Observable ما قرار میگیرد، تنها یک المان دارد که همان شیء HTTP Response است.
بنابراین کار متد map فوق، تبدیل شیء خروجی از سرور، به آرایهای از محصولات است.
در اینجا یک سری کدهای مدیریت استثناءها را نیز در صورت بروز مشکلی میتوان تعریف کرد. برای مثال در اینجا متد catch، کار پردازش خطاهای رخ داده را انجام میدهد.
از متد do جهت لاگ کردن عملیات رخ داده و دادههای دریافتی در کنسول developer tools مرورگرها استفاده شدهاست.
یک نکته:
اگر خروجی JSON از سرور، برای مثال داخل خاصیتی به نام data محصور شده بود، بجای ()response.json میبایستی از response.json().data استفاده میشد.
گوش فرا دادن به Observable دریافتی از سرور
تا اینجا یک درخواست HTTP GET را به سمت سرور ارسال کردیم و خروجی آن به صورت Observable در اختیار ما است. اکنون نیاز است کدهای ProductListComponent را جهت گوش فرا دادن به این Observable تغییر دهیم. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
errorMessage: string; ngOnInit(): void { //console.log('In OnInit'); this._productService.getProducts() .subscribe( products => this.products = products, error => this.errorMessage = <any>error); }
اولین پارامتر متد subscribe، کار دریافت نتایج حاصل را به عهده دارد. برای مثال اگر حاصل عملیات در طی سه مرحله صورت گیرد، سه بار نتیجهی دریافتی را میتوان در اینجا پردازش کرد. البته همانطور که عنوان شد، یک عملیات غیرهمزمان HTTP، تنها در طی یک مرحله، HTTP Response را دریافت میکند؛ بنابراین، پارامتر اول متد subscribe نیز تنها یکبار اجرا میشود. در اینجا فرصت خواهیم داشت تا آرایهی دریافتی حاصل از متد map قسمت قبل را به خاصیت عمومی products کلاس جاری نسبت دهیم.
پارامتر دوم متد subscribe در صورت شکست عملیات فراخوانی میشود. در اینجا حاصل آن به خاصیت جدید errorMessage نسبت داده شدهاست.
اکنون برنامه را مجددا اجرا کنید، هنوز باید لیست محصولات، مانند قبل نمایش داده شود.
یک نکته
اگر برنامه را اجرا کردید و خروجی مشاهده نشد، به کنسول developer tools مرورگر مراجعه کنید؛ احتمالا خطای ذیل در آن درج شدهاست:
EXCEPTION: No provider for Http!
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part8.zip
خلاصهی بحث
برای کار با سرور و ارسال درخواستهای HTTP، ابتدا نیاز است مدخل تعریف http.dev.js به index.html اضافه شود و سپس HTTP_PROVIDERS را در بالاترین سطح کامپوننتهای تعریف شده، ثبت و معرفی کرد. پس از آن نیاز است RxJs را نیز import کرد. در ادامه، سرویس دریافت لیست محصولات، وابستگی سرویس HTTP را توسط سازندهی خود دریافت کرده و از آن برای صدور یک فرمان HTTP GET استفاده میکند. سپس با استفاده از متد map، کار نگاشت شیء Response دریافتی، به فرمت مناسب مدنظر، صورت میگیرد.
در ادامه هر کلاسی که نیاز دارد با این کلاس سرویس دریافت اطلاعات کار کند، متد subscribe را فراخوانی کرده و نتیجهی عملیات را پردازش میکند.
در ابتدا مثالهای زیر را در نظر بگیرید:
using System;
using System.Collections.Generic;
using System.Linq;
namespace testWinForms87
{
public class Data
{
public int id { get; set; }
public string name { get; set; }
}
class CLinqTests
{
public static int TestGetListMin1()
{
var lst = new List<Data>
{
new Data{ id=1, name="id1"},
new Data{ id=2, name="id2"},
new Data{ id=3, name="name3"}
};
return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
public static int TestGetListMin2()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).Min();
}
}
}
محاسبات آن کار میکند و مشکلی هم ندارد. اما همیشه در دنیای واقعی همه چیز قرار نیست به این خوبی پیش برود. ممکن است همانند متد TestGetListMin2 ، لیست ما خالی باشد (برای مثال از دیتابیس، رکوردی مطابق شرایط کوئریهای قبلی بازگشت داده نشده باشد). در این حالت هنگام فراخوانی متد Min ، استثنای Sequence contains no elements رخ خواهد داد و همانطور که در مباحث defensive programming عنوان شد، وظیفهی ما این نیست که خودرو را به دیوار کوبیده (یا منتظر شویم تا کوبیده شود) و سپس به فکر چاره بیفتیم که خوب، عجب! مشکلی رخ داده است!
اکنون چه باید کرد؟ حداقل یک مرحله بررسی اینکه آیا کوئری ما حاوی رکوردی میباشد یا خیر باید به این متد اضافه شود (به صورت زیر):
public static int TestGetListMin3()
{
var lst = new List<Data>();
var query = from c in lst
where c.name.Contains("id")
select c.id;
if (query.Any())
return query.Min();
else
return -1;
}
شبیه به این مورد در هنگام استفاده از تابع Single مربوط به LINQ نیز ممکن است رخ دهد (تولید استثنای ذکر شده) اما در اینجا مایکروسافت تابع SingleOrDefault را نیز پیش بینی کرده است. در این حالت اگر کوئری ما رکوردی را برنگرداند، SingleOrDefault مقدار نال را برگشت داده و استثنایی رخ نخواهد داد (نمونهی دیگر آن متدهای First و FirstOrDefault هستند).
در مورد متدهای Min و Max ، متدهای MinOrDefault یا MaxOrDefault در دات نت فریم ورک وجود ندارند. میتوان این نقیصه را با استفاده از extension methods برطرف کرد.
using System;
using System.Collections.Generic;
using System.Linq;
public static class LinqExtensions
{
public static T MinOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Min<T>();
return defaultValue;
}
public static T MaxOrDefault<T>(this IEnumerable<T> source, T defaultValue)
{
if (source.Any<T>())
return source.Max<T>();
return defaultValue;
}
}
public static int TestGetListMin4()
{
var lst = new List<Data>();
return (from c in lst
where c.name.Contains("id")
select c.id).MinOrDefault(-1);
}
در ابتدای بحث، برای آشنایی بیشتر با HTML Helperها به مطالعه این مقاله بپردازین.
در این مقاله قرار است برای یک HTML Helper خاص، قالب نمایشی اختصاصی خودمان را طراحی کنیم و به نحوی HTML Helper موجود را سفارشی سازی کنیم. به عنوان مثال میخواهیم خروجی یک EditorFor() برای یک نوع خاص، به حالت دلخواهی باشد که ما خودمان آن را تولیدش کردیم؛ یا اصلا نه. حتی میشود برای خروجی یک EditorFor() که خصوصیتی از جنس string را میخواهیم به آن انتساب دهیم، به جای تولید input، یک مقدار متنی را برگردانیم. به این حالت:
<div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div>
در ادامه یک پروژهی عملی را شروع کرده و در آن کاری را که میخواهیم، انجام میدهیم. پروژهی ما به این شکل میباشد که قرار است در آن به ثبت کتاب بپردازیم و برای هر کتاب هم یک سبک داریم و قسمت سبک کتابهای ما یک Enum است که از قبل میخواهیم مقدارهایش را تعریف کنیم.
مدل برنامه
public class Books { public int Id { get; set; } [Required] [StringLength(255)] public string Name { get; set; } public Genre Genre { get; set; } }
public enum Genre { [Display(Name = "Non Fiction")] NonFiction, Romance, Action, [Display(Name = "Science Fiction")] ScienceFiction }
در داخل کلاس Books یک خصوصیت از جنس Genre برای سبک کتابها داریم و در داخل نوع شمارشی Genre، سبکهای ما تعریف شدهاند. همچنین هر کدام از سبکها هم به ویژگی Display مزین شدهاند تا بتونیم بعدا از مقدار آنها استفاده کنیم.
کنترلر برنامه
public class BookController : Controller { // GET: Book public ActionResult Index() { return View(DataAccess.DataContext.Book.ToList()); } public ActionResult Create() { return View(); } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.Add(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Edit(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit(Books model) { if (!ModelState.IsValid) return View(model); try { DataAccess.DataContext.Book.AddOrUpdate(model); DataAccess.DataContext.SaveChanges(); return RedirectToAction("Index"); } catch (Exception ex) { ModelState.AddModelError("", ex.Message); return View(model); } } public ActionResult Details(int id) { try { var book = DataAccess.DataContext.Book.Find(id); return View(book); } catch (Exception ex) { return View("Error"); } } }
در قسمت کنترلر هم کار خاصی جز عملیات اصلی نوشته نشدهاست. لیست کتابها را از پایگاه داده بیرون آوردیم و از طریق اکشن Index به نمایش گذاشتیم. با اکشنهای Create، Edit و Details هم کارهای روتین مربوط به خودشان را انجام دادیم. نکتهی قابل تذکر، DataAccess میباشد که کلاسی است که با آن ارتباط برقرار شده با EF و سپس اطلاعات واکشی و تزریق میشوند.
View مربوط به اکشن Create برنامه
@using Book.Entities @model Book.Entities.Books @{ ViewBag.Title = "Create"; } <h2>New Book</h2> @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <h4>Books</h4> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div> @Html.LabelFor(model => model.Name, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Name, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Name, "", new { @class = "text-danger" }) </div> </div> <div> @Html.LabelFor(model => model.Genre, htmlAttributes: new { @class = "control-label col-md-2" }) <div> @Html.EditorFor(model => model.Genre, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Genre, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="Create" /> <input type="reset" value="Reset" /> @Html.ActionLink("Back to List", "Index", null, new {@class="btn btn-default"}) </div> </div> </div> } @section Scripts { @Scripts.Render("~/bundles/jqueryval") }
View برنامه هم همان ویویی است که خود Visual Studio برای ما ساختهاست. به جز یک سری دستکاریهایی داخل سیاساس، هدف از گذاشتن View مربوط به Create این بود که قرار است بر روی این قسمت کار کنیم. اگر پروژه رو اجرا کنید و به قسمت Create بروید، مشاهده خواهید کرد که برای Genre یک input ساخته شدهاست که کاربر باید در آن مقدار وارد کند. ولی اگر یادتان باشد، ما سبکهای نگارشی خودمان را در نوع شمارشی Genre ایجاد کرده بودیم. پس عملا باید یک لیست به کاربر نشان داده شود که تا از آن لیست، نوع را انتخاب کند. میتوانیم بیایم همینجا در داخل View مربوطه، بهجای استفاده از HTML Helper پیشفرض، از DropDownList یا EnumFor استفاده کنیم و به طریقی این لیست را ایجاد کنیم. ولی چون قرار است در این مثال به شرح موضوع مقاله خودمان بپردازیم، این کار را انجام نمیدهیم.
در حقیقیت میخوایم متد EditorFor را طوری سفارشی سازی کنیم که برای نوع شمارشی Genre، به صورت خودکار یک لیست ایجاد کرده و برگرداند. از نسخهی سوم ASP.NET MVC به بعد این امکان برای توسعه دهندهها فراهم شدهاست. شما میتوانید در پوشهی Shared داخل پوشه Views برنامه، پوشهای را به اسم EditorTemplates ایجاد کنید؛ همینطور DisplayTemplates و برای نوع خاصی که میخواهید سفارشیسازی را برای آن انجام دهید، یک PartialView بسازید.
Views/Shared/DisplayTemplates/<type>.cshtml
کاری که الان میخواهیم انجام دهیم این است که یک SelectListItem ایجاد کرده تا مقدارهای نوع Genreمان داخلش باشد و بتوانیم به راحتی برای ساختن DropDownList از آن استفاده کنیم. برای این کار Helper مخصوص خودمان را ایجاد میکنیم. پوشهای به اسم Helpers در کنار پوشههای Controllers، Models ایجاد میکنیم و در داخل آن کلاسی به اسم EnumHelpers میسازیم.
public static class EnumHelpers { public static IEnumerable<SelectListItem> GetItems( this Type enumType, int? selectedValue) { if (!typeof(Enum).IsAssignableFrom(enumType)) { throw new ArgumentException("Type must be an enum"); } var names = Enum.GetNames(enumType); var values = Enum.GetValues(enumType).Cast<int>(); var items = names.Zip(values, (name, value) => new SelectListItem { Text = GetName(enumType, name), Value = value.ToString(), Selected = value == selectedValue } ); return items; } static string GetName(Type enumType, string name) { var result = name; var attribute = enumType .GetField(name) .GetCustomAttributes(inherit: false) .OfType<DisplayAttribute>() .FirstOrDefault(); if (attribute != null) { result = attribute.GetName(); } return result; } }
در توضیح کد بالا عنوان کرد که متدها بهصورت متدهای الحاقی به نوع Type نوشته شدند. کار خاصی در بدنهی متدها انجام نشدهاست. در بدنهی متد اول لیست آیتمها را تولید کردیم. در هنگام ساخت SelectListItem برای گرفتن Text، متد GetName را صدا زدیم. برای اینکه بتوانیم مقدار ویژگی Display که در هنگام تعریف نوع شمارشی استفاده کردیم را بدست بیاریم، باید چک کنیم ببینیم که آیا این آیتم به این ویژگی مزین شدهاست یا نه. اگر شده بود مقدار را میگیریم و به خصوصیت Text متد اول انتساب میدهیم.
@using Book.Entities @using Book.Web.Helpers @{ var items = typeof(Genre).GetItems((int?)Model); } @Html.DropDownList("", items, new {@class="form-control"})
کدهایی که در بالا مشاهده میکنید کدهایی میباشند که قرار است داخل PartialViewی Genre قرار دهیم که در پوشهی EditorTemplates ساختیم. ابتدا آمدیم آیتمها را گرفتیم و بعد به DropDownList دادیم تا لیست نوع را برای ما بسازد. حالا اگه برنامه را اجرا کنید میبینید که EditorFor برای شما یه لیست از نوع شمارشی ساخته و حالا قابل استفاده هست.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.Options; namespace OpenAPISwaggerDoc.Web.Authentication; public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { private string _failReason; public BasicAuthenticationHandler( IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { } protected override Task<AuthenticateResult> HandleAuthenticateAsync() { if (!Request.Headers.ContainsKey("Authorization")) { _failReason = "Missing Authorization header"; return Task.FromResult(AuthenticateResult.Fail(_failReason)); } try { var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter); var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':'); var username = credentials[0]; var password = credentials[1]; if (string.Equals(username, "DNT", StringComparison.Ordinal) && string.Equals(password, "123", StringComparison.Ordinal)) { var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } _failReason = "Invalid username or password"; return Task.FromResult(AuthenticateResult.Fail(_failReason)); } catch { _failReason = "Invalid Authorization header"; return Task.FromResult(AuthenticateResult.Fail(_failReason)); } } protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { await base.HandleChallengeAsync(properties); if (Response.StatusCode == StatusCodes.Status401Unauthorized && !string.IsNullOrWhiteSpace(_failReason)) { Response.Headers.Add("WWW-Authenticate", _failReason); Response.ContentType = "application/json"; await WriteProblemDetailsAsync(_failReason); } } private Task WriteProblemDetailsAsync(string detail) { var problemDetails = new ProblemDetails { Detail = detail, Status = Context.Response.StatusCode }; var result = new ObjectResult(problemDetails) { ContentTypes = new MediaTypeCollection(), StatusCode = problemDetails.Status, DeclaredType = problemDetails.GetType(), }; var executor = Context.RequestServices.GetRequiredService<IActionResultExecutor<ObjectResult>>(); var routeData = Context.GetRouteData() ?? new RouteData(); var actionContext = new ActionContext(Context, routeData, new ActionDescriptor()); return executor.ExecuteAsync(actionContext, result); } }
public string GetTranscriptsUrl() { var courseName = getCourseName(); return $"http://app.pluralsight.com/data/course/transcript/{courseName}/en"; }
public string GetTranscriptsUrl()
{
var courseName = getCourseName();
return $"http://app.pluralsight.com/data/course/transcript/{courseName}/fa";
}
ساختار مورد نیاز یک Kendo UI Tree View
فرض کنید قصد دارید نظرات تو در توی مطلبی را توسط Kendo UI Tree View نمایش دهید. مدل خود ارجاع دهندهی آن میتواند چنین شکلی را داشته باشد:
namespace KendoUI11.Models { public class BlogComment { public int Id { set; get; } public string Body { set; get; } public int? ParentId { get; set; } // مخصوص کندو یو آی هستند public bool HasChildren { get; set; } public string imageUrl { get; set; } } }
چند خاصیت بعدی مانند HasChildren و imageUrl مخصوص Kendo UI هستند. از imageUrl اختیاری میتوان جهت نمایش آیکنی در کنار یک آیتم استفاده کرد و HasChildren به این معنا است که آیا گره جاری دارای عناصر فرزندی میباشد یا خیر.
تهیه یک منبع داده نمونه
شکل ابتدای مطلب، از طریق منبع داده ذیل تهیه شدهاست:
using System.Collections.Generic; namespace KendoUI11.Models { /// <summary> /// منبع داده فرضی جهت سهولت دموی برنامه /// </summary> public static class BlogCommentsDataSource { private static readonly IList<BlogComment> _cachedItems; static BlogCommentsDataSource() { _cachedItems = createBlogCommentsDataSource(); } public static IList<BlogComment> LatestComments { get { return _cachedItems; } } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogComment> createBlogCommentsDataSource() { var list = new List<BlogComment>(); var comment1 = new BlogComment { Id = 1, Body = "نظر من این است که", HasChildren = true, ParentId = null }; list.Add(comment1); var comment12 = new BlogComment { Id = 2, Body = "پاسخی به نظر اول", HasChildren = true, ParentId = 1 }; list.Add(comment12); var comment12A = new BlogComment { Id = 3, Body = "پاسخی دیگری به نظر اول", HasChildren = false, ParentId = 1 }; list.Add(comment12A); var comment121 = new BlogComment { Id = 4, Body = "پاسخی به پاسخ به نظر اول", HasChildren = false, ParentId = 2 }; list.Add(comment121); var comment2 = new BlogComment { Id = 5, Body = "نظر 2", HasChildren = true, ParentId = null, imageUrl= "images/search.png" }; list.Add(comment2); var comment21 = new BlogComment { Id = 6, Body = "پاسخ به نظر 2", HasChildren = false, ParentId = 5 }; list.Add(comment21); return list; } } }
در این لیست دو رکورد، دارای ParentId مساوی null هستند. از این null بودنها جهت کوئری گرفتن و نمایش ریشههای TreeView در ادامه استفاده خواهیم کرد.
بازگشت نظرات با فرمت JSON به سمت کلاینت
در ادامه یک کنترلر ASP.NET MVC را مشاهده میکنید که توسط اکشن متد GetBlogComments، رکوردهای مورد نظر را با فرمت JSON به سمت کلاینت ارسال میکند:
using System.Linq; using System.Web.Mvc; using KendoUI11.Models; namespace KendoUI11.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); // shows the page. } [HttpGet] public ActionResult GetBlogComments(int? id) { if (id == null) { //دریافت ریشهها return Json( BlogCommentsDataSource.LatestComments .Where(x => x.ParentId == null) // ریشهها .ToList(), JsonRequestBehavior.AllowGet); } else { //دریافت فرزندهای یک ریشه return Json( BlogCommentsDataSource.LatestComments .Where(x => x.ParentId == id) .ToList(), JsonRequestBehavior.AllowGet); } } } }
اگر مقدار id به سمت سرور ارسال شود، یعنی کاربر گره و نودی را گشودهاست. بر این اساس، تمامی فرزندان این گره را یافته و بازگشت میدهیم.
کدهای سمت کاربر نمایش Kendo UI Tree View
برای کار با Kendo UI TreeView نیاز است از منبع داده خاصی به نام HierarchicalDataSource به نحو ذیل استفاده کنیم. در قسمت transport آن مشخص میکنیم که اطلاعات باید از چه آدرسی خوانده شوند که در اینجا به آدرس اکشن متد GetBlogComments اشاره میکند.
همچنین نیاز است مشخص کنیم کدامیک از خواص مدل بازگردانده شده، همان hasChildren است که در مثال فوق دقیقا به همین نام نیز تنظیم شدهاست.
<!--نحوهی راست به چپ سازی --> <div class="k-rtl k-header demo-section"> <div id="my-treeview"></div> </div> @section JavaScript { <script type="text/javascript"> $(function () { var dataSource = new kendo.data.HierarchicalDataSource({ transport: { read: { url: "@Url.Action("GetBlogComments", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } }, schema: { model: { id: "Id", hasChildren: "HasChildren" } } }); $("#my-treeview").kendoTreeView({ //استفاده از قالب در صورت نیاز template: kendo.template($("#treeview-template").html()), checkboxes: { checkChildren: false }, dataSource: dataSource, dataTextField: "Body", //رخدادها select: function (e) { console.log("Selecting: " + this.text(e.node)); }, check: function (e) { console.log("Checkbox changed :: " + this.text(e.node)); }, change: function (e) { console.log("Selection changed"); }, collapse: function (e) { console.log("Collapsing " + this.text(e.node)); }, expand: function (e) { console.log("Expanding " + this.text(e.node)); } }); }); </script> <script id="treeview-template" type="text/kendo-ui-template"> <strong> #: item.Body # </strong> </script> <style scoped> .demo-section { width: 100%; height: 300px; } </style> }
- در ابتدا به ازای هر ردیف این TreeView، از یک قالب استفاده شدهاست. تعریف این مورد اختیاری است. اگر نیاز به سفارشی سازی نحوهی نمایش هر آیتم را داشتید، میتوان از قالبها استفاده کرد.
- قسمت checkboxes مشخص میکند که آیا نیاز است در کنار هر آیتم یک checkbox نیز نمایش داده شود یا خیر.
- dataSource را به HierarchicalDataSource تنظیم کردهایم.
- dataTextField مشخص میکند که کدام فیلد دربرگیرندهی متن هر آیتم TreeView است.
- تعدادی رخداد منتسب به TreeView نیز تنظیم شدهاند که خروجی آنها را در console تصویر ابتدای بحث مشاهده میکنید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.