تهیه مقدمات سمت سرور
مدلی که در تصویر فوق نمایش داده شدهاست، در سمت سرور چنین ساختاری را دارد:
namespace AngularTemplateDrivenFormsLab.Models { public class Product { public int ProductId { set; get; } public string ProductName { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
همچنین یک منبع ساده درون حافظهای را نیز جهت بازگشت 1500 محصول تهیه کردهایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
using System.Collections.Generic; namespace AngularTemplateDrivenFormsLab.Models { public static class ProductDataSource { private static readonly IList<Product> _cachedItems; static ProductDataSource() { _cachedItems = createProductsDataSource(); } public static IList<Product> LatestProducts { get { return _cachedItems; } } private static IList<Product> createProductsDataSource() { var list = new List<Product>(); for (var i = 0; i < 1500; i++) { list.Add(new Product { ProductId = i + 1, ProductName = "نام " + (i + 1), IsAvailable = (i % 2 == 0), Price = 1000 + i }); } return list; } } }
مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت
زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال میکند، یک چنین ساختاری را دریافت خواهیم کرد:
http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگها تهیه میکنیم:
public interface IPagedQueryModel { string SortBy { get; set; } bool IsAscending { get; set; } int Page { get; set; } int PageSize { get; set; } }
کاهش کدهای تکراری صفحه بندی اطلاعات در سمت سرور
با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آنها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
public class ProductQueryViewModel : IPagedQueryModel { // ... other properties ... public string SortBy { get; set; } public bool IsAscending { get; set; } public int Page { get; set; } public int PageSize { get; set; } }
ب) امکان استفادهی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
public static class IQueryableExtensions { public static IQueryable<T> ApplyPaging<T>( this IQueryable<T> query, IPagedQueryModel model) { if (model.Page <= 0) { model.Page = 1; } if (model.PageSize <= 0) { model.PageSize = 10; } return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize); } }
همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیفهای هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شدهاست. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحهی اول اطلاعات نمایش داده میشود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).
کاهش کدهای تکراری مرتب سازی اطلاعات در سمت سرور
همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
if(model.SortBy == "f1") { query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1); }
اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آنها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.
یک نمونه از مزیتهای تهیهی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونهی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
namespace AngularTemplateDrivenFormsLab.Utils { public static class IQueryableExtensions { public static IQueryable<T> ApplyOrdering<T>( this IQueryable<T> query, IPagedQueryModel model, IDictionary<string, Expression<Func<T, object>>> columnsMap) { if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy)) { return query; } if (model.IsAscending) { return query.OrderBy(columnsMap[model.SortBy]); } else { return query.OrderByDescending(columnsMap[model.SortBy]); } } } }
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap);
تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت
تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه میخواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
using System.Collections.Generic; namespace AngularTemplateDrivenFormsLab.Models { public class PagedQueryResult<T> { public int TotalItems { get; set; } public IEnumerable<T> Items { get; set; } } }
پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts
در اینجا اکشن متدی را مشاهده میکنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت میدهد:
[Route("api/[controller]")] public class ProductController : Controller { [HttpGet("[action]")] public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel) { var pagedResult = new PagedQueryResult<Product>(); var query = ProductDataSource.LatestProducts .AsQueryable(); //TODO: Apply Filtering ... .where(p => p....) ... var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap); pagedResult.TotalItems = query.Count(); query = query.ApplyPaging(queryModel); pagedResult.Items = query.ToList(); return pagedResult; } }
امضای این اکشن متد، شامل دو مورد مهم است:
public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتمهای صفحهی جاری درخواستی باشد.
در ابتدای کار، دسترسی به منبع دادهی درون حافظهای ابتدای برنامه را مشاهده میکنید. برای اینکه کارکرد آنرا شبیه به کوئریهای ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شدهاست.
var query = ProductDataSource.LatestProducts .AsQueryable(); //TODO: Apply Filtering ... .where(p => p....) ...
پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>() { ["productId"] = p => p.ProductId, ["productName"] = p => p.ProductName, ["isAvailable"] = p => p.IsAvailable, ["price"] = p => p.Price }; query = query.ApplyOrdering(queryModel, columnsMap);
در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتمهای منبع داده محاسبه شدهاست و سپس صفحه بندی به آن اعمال گردیدهاست. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیفهای صفحهی جاری محاسبه میشود:
var pagedResult = new PagedQueryResult<Product>(); pagedResult.TotalItems = query.Count(); query = query.ApplyPaging(queryModel); pagedResult.Items = query.ToList(); return pagedResult;
در قسمت بعد، نحوهی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.