public class LockFilter : ActionFilterAttribute { static ConcurrentDictionary<StringBuilder, int> _properties; static LockFilter() { _properties = new ConcurrentDictionary<StringBuilder, int>(); } public int Duration { get; set; } public string VaryByParam { get; set; } public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var actionArguments = context.ActionArguments.Values.Single(); var properties = VaryByParam.Split(",").ToList(); StringBuilder key = new StringBuilder(); foreach (var actionArgument in actionArguments.GetType().GetProperties()) { if (!properties.Any(t => t.Trim().ToLower() == actionArgument.Name.ToLower())) continue; var value = actionArguments.GetType().GetProperty(actionArgument.Name).GetValue(actionArguments, null).ToString(); key.Append(value); } _properties.AddOrUpdate(key, 1, (x, y) => y + 1); // rest of code } }
در SampleProject1 مدل Product را داریم:
public partial class Product : Entity { public int Id { get; set; } public string Name { get; set; } public Nullable<byte> ProductTypeId { get; set; } }
public partial class ProductType : Entity { public byte Id { get; set; } public string Name { get; set; } }
List<Assembly> allAssemblies = new List<Assembly>(); string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); foreach (string dll in Directory.GetFiles(path, "*.Common.dll")) allAssemblies.Add(Assembly.LoadFile(dll)); var type = typeof(Entity); List<Type> types = allAssemblies .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p)).ToList(); List<string> entities = new List<string>(); foreach (var item in types) { entities.Add(item.Name); } types.Add(typeof(Entity));
public class ContextGenerator { public void Generate(List<string> entities, params Type[] types) { StringBuilder code = new StringBuilder(); code.AppendLine(@" using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using SampleProject1.Common.Models; using SampleProject1.Common.Models.Mapping; using SampleProject2.Common.Models; using SampleProject2.Common.Models.Mapping; namespace DbContextGenerator { public partial class TestContext : DbContext { static TestContext() { Database.SetInitializer<TestContext>(null); } public TestContext() : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"") { } "); var pluralizeHelper = new PluralizeHelper(); foreach (var entity in entities) { code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}"); } code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)"); code.AppendLine(@"{"); foreach (var entity in entities) { code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());"); } code.AppendLine(@"}"); code.AppendLine(@"}"); code.AppendLine(@"}"); CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters(); parameters.ReferencedAssemblies.Add("System.Drawing.dll"); parameters.ReferencedAssemblies.Add("System.Data.dll"); parameters.ReferencedAssemblies.Add("System.Data.Entity.dll"); parameters.ReferencedAssemblies.Add("System.ComponentModel.dll"); foreach (var type in types) { parameters.ReferencedAssemblies.Add(type.Assembly.Location); } parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location); parameters.GenerateExecutable = false; parameters.GenerateInMemory = false; parameters.OutputAssembly = "ProjectContext.dll"; CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString()); if (results.Errors.HasErrors) { StringBuilder sb = new StringBuilder(); foreach (CompilerError error in results.Errors) { sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)); } throw new InvalidOperationException(sb.ToString()); } } }
new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:
static DbContext _dbContext=null; public static DbContext GetDbContextInstance() { if (_dbContext == null) { string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll"); Type type = dllversionAssm.GetType("DbContextGenerator.TestContext"); _dbContext = (DbContext)Activator.CreateInstance(type); } return _dbContext; }
و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:
public static System.Data.Entity.DbSet<T> Get<T>() where T : class { var set = GetDbContextInstance().Set<T>(); return set; }
هم اکنون میتوان رکوردهای Entityها را واکشی کرده و یا آنها را با یکدیگر Join بزنیم:
var products = Get<Product>().ToList(); var productTypes = Get<ProductType>().ToList(); var query = from p in Get<Product>() join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id select new { Id = p.Id, Name = p.Name, ProductType = pt.Name }; var JoinResult = query.ToList();
و نتیجه واکشی ها
برای تنظیم پویای عنوان یک صفحهی وب، نیاز است با DOM API مرورگر به صورت مستقیم کار کرد. برای مثال فایل wwwroot\main.js را که مدخل آن به کامپوننت Host_ و یا صفحهی index.html اضافه میشود، به صورت زیر تکمیل میکنیم:
window.JsFunctionHelper = { blazorSetTitle: function (title) { document.title = title; } };
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnParametersSetAsync() { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
@page "/counter" <PageTitle Title="@GetPageTitle()" /> <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; } private string GetPageTitle() => $"Counter ({currentCount})"; }
برای رفع این مشکل همانطور که در مطلب جاری نیز عنوان شد، باید از روال رویدادگردان OnAfterRenderAsync استفاده کرد. در این حالت کدهای کامپوننت PageTitle.razor به صورت زیر تغییر میکنند:
@inject IJSRuntime JSRuntime @code { [Parameter] public string Title { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { await JSRuntime.InvokeVoidAsync("JsFunctionHelper.blazorSetTitle", Title); } }
یک نکته: قرار است در Blazor 6x، کامپوننتهای جدید Title، Link و Meta جهت تنظیم اطلاعات تگ head صفحه، به صورت استاندارد اضافه شوند:
<Title Value="@title" /> <Meta name="description" content="Modifying the head from a Blazor component." /> <Link href="main.css" rel="stylesheet" />
فیلترها در MVC
فیلتر، یک کلاس سفارشی است که شما میتوانید منطق برنامه را جهت اجرا، قبل یا بعد از اجرای یک اکشن متد، در آن پیاده سازی نمایید. فیلترها میتوانند به یک اکشن متد و یا کنترلری منتسب شوند که در ادامه با این روشها آشنا خواهید شد.
در لیست زیر انواع فیلترها و اینترفیسهایی که باید توسط کلاس سفارشی شما پیاده سازی شوند، معرفی شده است.
نوع | توضیح | فیلتر توکار | اینترفیس |
Authorization | انجام عملیات احراز هویت و سطح دسترسی، قبل از اجرای کد اکشن متد | [Authorize] و [RequireHttps] | IAuthorizationFilter |
Action | اجرای کدهایی قبل از اجرای کدهای اکشن متد | IActionFilter | |
Result | اجرای کدهایی قبل یا بعد از تولید ویو (View result) | [OutputCache] | IResultFilter |
Exception | اجرای کدهایی در صورت وجود استثنای مدیریت نشده | [HandleError] | IExceptionFilter |
در تکه کد زیر نحوهی استفاده از این فیلتر را مشاهده میکنید:
[HandleError] public class HomeController : Controller { public ActionResult Index() { //throw exception for demo throw new Exception("This is unhandled exception"); return View(); } public ActionResult About() { return View(); } public ActionResult Contact() { return View(); } }
نکته: فیلترهای اعمال شدهی به یک کنترلر، به تمام اکشن متدهای آن نیز اعمال میگردند.
در کد بالا خصیصهی HandleError به HomeController اعمال شده است. بنابراین در صورت بروز خطایی در هر کدام از اکشنها، صفحهی Error.cshtml نمایش داده خواهد شد و در تظر داشته باشید که خطاها توسط try-catch هندل نشدهاند.
باید جهت عملکرد صحیح فیلتر توکار HandleErrorAttribute، مقدار customErrors در قسمت System.web فایل web.config مساوی on باشد.
<customErrors mode="On" />
مهیا کنندگان فیلترها
بصورت پیش فرض MVC از سه طریق زیر فیلترها را جهت استفادهی در برنامه فراهم میکند:
- خصیصهی GlobalFilters.Filters برای فیلترهای سراسری
- کلاس FilterAttributeFilterProvider برای فیلترهای خصیصهای
- کلاس ControllerInstanceFilterProvider جهت افزودن کنترلر به یک وهله از FilterProviderCollection
در ادامه با نحوهی ایجاد یک فیلتر، بوسیلهی هر یک از روشهای بالا، با ذکر مثالی بیشتر آشنا خواهید شد.
ترتیب اجرای فیلترها
همانطور که در ابتدا اشاره شد، در MVC چهار نوع فیلتر معرفی شده است که امکان استفادهی از آنها بهصورت همزمان در سطح کنترلر و یا اکشن متد وجود دارد. اما ترتیب اجرای آنها متفاوت و به ترتیب زیر است:
- فیلترهای Authorization
- فیلترهای Action
- فیلترهای Result یا Response
- فیلترهای Exception
فیلترها براساس ترتیب اشاره شدهی در بالا اجرا خواهند شد. در صورتیکه چند فیلتر از یک نوع استفاده شود، جهت تقدم و تاخر در اجرا، از خاصیت Order استفاده خواهد شد. بعنوان مثال در کد زیر بدلیل خاصیت Order=1 ابتدا AuthorizationFilterB و سپس AuthorizationFilterA اجرا میشود.
[AuthorizationFilterA(Order=2)] [AuthorizationFilterB(Order=1)] public ActionResult Index() { return View(); }
public enum FilterScope { First = 0, Global = 10, Controller = 20, Action = 30, Last = 100, }
نکته: مقدار Scope فیلترهای Authorization برابر 0 و فیلترهای Exception برابر 100 میباشد.
ایجاد فیلتر سفارشی
روش اول: پیاده سازی اینترفیس یکی از انواع فیلترها و ارث بری از کلاس FilterAttribute
در این روش متدهایی که باید پیاده سازی شوند متفاوت خواهد بود. به همین جهت متدهای هر نوع بشرح زیر معرفی میشود:
- IAuthorizationFilter
// Called when authorization is required void OnAuthorization(AuthorizationContext filterContext)
- IActionFilter
// Called after the action method executes void OnActionExecuted(ActionExecutedContext filterContext) // Called before an action method executes void OnActionExecuting(ActionExecutingContext filterContext)
- IResultFilter
// Called after an action result executes void OnResultExecuted(ResultExecutedContext filterContext) // Called before an action result executes void OnResultExecuting(ResultExecutedContext filterContext)
- IException
// Called when an exception occurs void OnException(ExceptionContext filterContext)
یادآوری: همانطور که در ابتدای مقاله اشاره شد، فیلترها قبل یا بعد از اجرای اکشن متدها فراخوانی خواهند شد. بنابراین به کامنت بالای متد فیلترها دقت داشته باشید.
مثال: پیاده سازی اینترفیس IExceptionFilter و ارث بری از کلاس FilterAttribute جهت تهیهی فیلتری سفارشی از نوع Exception
class CustomErrorHandler : FilterAttribute, IExceptionFilter { public override void IExceptionFilter.OnException(ExceptionContext filterContext) { Log(filterContext.Exception); base.OnException(filterContext); } private void Log(Exception exception) { //log exception here.. } }
روش دوم: ارث بری از ActionFilterAttribute
کلاس abstract فوق دارای چهار متد زیر جهت تحریف است. همانطور که مشاهده میکنید این کلاس علاوه بر دو متد OnActionExecuted و OnActionExecuting دارای دو متد دیگر OnResultExecuting و OnResultExecuted که بهترتیب قبل و بعد خروجی (Result) اکشن متد اجرا میشوند، نیز میباشد. این نوع فیلترها عموما جنبهی استفاده عمومی داشته و میتوان از آنها جهت logging ،caching و یا authorization استفاده کرد.
// Called by MVC after the action method executes void OnActionExecuted(ActionExecutedContext filterContext) // Called by MVC before the action method executes void OnActionExecuted(ActionExecutedContext filterContext) // Called by MVC after the action result executes void OnResultExecuted(ResultExecutedContext filterContext) // Called by MVC before the action result executes void OnResultExecuting(ResultExecutingContext filterContext)
مثال: کلاس LogAttribute که از کلاس ActionFilterAttribute ارث بری کرده است، عملیات قبل و بعد از اجرای اکشن متد را لاگ میکند.
public class LogAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext filterContext) { Log("OnActionExecuted", filterContext.RouteData); } public override void OnActionExecuting(ActionExecutingContext filterContext) { Log("OnActionExecuting", filterContext.RouteData); } public override void OnResultExecuted(ResultExecutedContext filterContext) { Log("OnResultExecuted", filterContext.RouteData); } public override void OnResultExecuting(ResultExecutingContext filterContext) { Log("OnResultExecuting ", filterContext.RouteData); } private void Log(string methodName, RouteData routeData) { var controllerName = routeData.Values["controller"]; var actionName = routeData.Values["action"]; var message = String.Format("{0}- controller:{1} action:{2}", methodName, controllerName, actionName); Debug.WriteLine(message); } }
روش سوم: پیاده سازی داخل کنترلر
کلاس Controller میتواند هر یک از اینترفیسهای فیلترها را پیاده سازی نماید. به عبارت دیگر در هر کلاس کنترلر میتوانید متدهای زیر را تحریف نمایید.
- OnAuthorization ^
- OnException ^
- OnActionExecuting ^
- OnActionExecuted ^
- OnResultExecuting ^
- OnResultExecuted ^
روش چهارم: ارث بری از کلاس فیلترهای توکار و مهیای در MVC و تحریف متدهای آن
در کد زیر با تحریف و سفارشی سازی متد OnException مخصوص فیلتر توکار HandleError، قابلیتهای آن افزایش یافته است:
class CustomErrorHandler : HandleErrorAttribute { public override void OnException(ExceptionContext filterContext) { Log(filterContext.Exception); base.OnException(filterContext); } private void Log(Exception exception) { //log exception here.. } }
رجیستر فیلترها
- سراسری:
درصورتی که قصد داشته باشید فیلتری بصورت سراسری و در کل برنامه فعال گردد باید آن را در رویداد Application_Start فایل Global.asax.cs بوسیلهی متد RegisterGlobalFilters کلاس FiterConfig رجیستر نمایید. بعد از آن فیلتر به کلیهی کنترلرها و اکشن متدها اعمال میگردد.
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); } } // FilterConfig.cs located in App_Start folder public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); // add your new custom filters filters.Add(new LogAttribute()); filters.Add(new CustomErrorHandler()); } }
در کد بالا فیلتر توکار HandleError و البته فیلترهای سفارشی دیگری نیز به صورت سراسری به تمام اکشن متدهای کنترلرها اعمال گردیده است.
- کنترلر: در صورتی که فقط بخواهید یک فیلتر به کل اکشنهای یک کنترلر اعمال گردد. همانند آنچه که در مثال ابتدایی بدان اشاره شد.
[HandleError] public class HomeController : Controller
- اکشن متد: اعمال یک فیلتر به یک اکشن متد خاص کنترلر. در کد زیر فیلتر HandleError فقط به اکشن متد Index کنترلر Home اعمال خواهد شد.
public class HomeController : Controller { [HandleError] public ActionResult Index() { return View(); } }
تهیه سرویس اطلاعات پویای برنامه
سرویس Web API ارائه شدهی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشتهای آنها به سمت کلاینت باز میگرداند و ساختار موجودیتهای آنها به صورت زیر است:
موجودیت کاربر که یک رابطهی one-to-many را با UserNotes دارد:
using System; using System.Collections.Generic; namespace MaterialAspNetCoreBackend.DomainClasses { public class User { public User() { UserNotes = new HashSet<UserNote>(); } public int Id { set; get; } public DateTimeOffset BirthDate { set; get; } public string Name { set; get; } public string Avatar { set; get; } public string Bio { set; get; } public ICollection<UserNote> UserNotes { set; get; } } }
using System; namespace MaterialAspNetCoreBackend.DomainClasses { public class UserNote { public int Id { set; get; } public DateTimeOffset Date { set; get; } public string Title { set; get; } public User User { set; get; } public int UserId { set; get; } } }
public interface IUsersService { Task<List<User>> GetAllUsersIncludeNotesAsync(); Task<User> GetUserIncludeNotesAsync(int id); }
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class UsersController : Controller { private readonly IUsersService _usersService; public UsersController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet] public async Task<IActionResult> Get() { return Ok(await _usersService.GetAllUsersIncludeNotesAsync()); } [HttpGet("{id:int}")] public async Task<IActionResult> Get(int id) { return Ok(await _usersService.GetUserIncludeNotesAsync(id)); } } }
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
https://localhost:5001/api/users
و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت میدهد.
تنظیم محل تولید خروجی Angular CLI
ساختار پوشه بندی پروژهی جاری به صورت زیر است:
همانطور که ملاحظه میکنید، کلاینت Angular در یک پوشهاست و برنامهی سمت سرور ASP.NET Core در پوشهای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشهی wwwroot پروژهی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش میکنیم:
"build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
ng build --no-delete-output-path --watch
dotnet watch run
بنابراین دو صفحهی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشهی MaterialAngularClient) و در دومی dotnet watch run را در پوشهی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا میشوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژهها ایجاد کردید، صرفا همان قسمت کامپایل میشود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.
تعریف معادلهای کلاسهای موجودیتهای سمت سرور، در برنامهی Angular
در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادلهای کلاسهای موجودیتهای سمت سرور خود را به صورت اینترفیسهایی تایپاسکریپتی تعریف کنیم:
ng g i contact-manager/models/user ng g i contact-manager/models/user-note
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote { id: number; title: string; date: Date; userId: number; }
import { UserNote } from "./user-note"; export interface User { id: number; birthDate: Date; name: string; avatar: string; bio: string; userNotes: UserNote[]; }
ایجاد سرویس Angular دریافت اطلاعات از سرور
ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد میکنیم:
ng g s contact-manager/services/user --no-spec
import { Injectable } from "@angular/core"; @Injectable({ providedIn: "root" }) export class UserService { constructor() { } }
کدهای تکمیل شدهی UserService را در ذیل مشاهده میکنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } getAllUsersIncludeNotes(): Observable<User[]> { return this.http .get<User[]>("/api/users").pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } getUserIncludeNotes(id: number): Observable<User> { return this.http .get<User>(`/api/users/${id}`).pipe( map(response => response || {} as User), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشتهای آنها از سرور واکشی میکند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشتهای او از سرور دریافت میکند.
بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material
بستهی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوهی «افزودن آیکنهای متریال به برنامه» را بررسی کردیم که در آنجا آیکنهای مرتبط، از فایلهای قلم، دریافت و نمایش داده میشوند. این کامپوننت، علاوه بر قلم آیکنها، از فایلهای svg حاوی آیکنها نیز پشتیبانی میکند که یک نمونه از این فایلها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شدهاست (چون برنامهی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?> <svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <svg viewBox="0 0 128 128" height="100%" width="100%" pointer-events="none" display="block" id="user1" >
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماسها را با صورت زیر تکمیل میکنیم:
import { Component } from "@angular/core"; import { MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; @Component() export class ContactManagerAppComponent { constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) { iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg")); } }
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon( "unicorn", this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg") );
<mat-icon svgIcon="unicorn"></mat-icon>
یک نکته: پوشهی node_modules\material-design-icons به همراه تعداد قابل ملاحظهای فایل svg نیز هست.
نمایش لیست کاربران در sidenav
در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق میکنیم:
import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component() export class SidenavComponent implements OnInit { users: User[] = []; constructor(private userService: UserService) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => this.users = data); } }
اکنون میخواهیم از این اطلاعات جهت نمایش پویای آنها در sidenav استفاده کنیم. در قسمت قبل، جای آنها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
<mat-list> <mat-list-item>Item 1</mat-list-item> <mat-list-item>Item 2</mat-list-item> <mat-list-item>Item 3</mat-list-item> </mat-list>
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine href="#"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آنها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.
اتصال کاربران به صفحهی نمایش جزئیات آنها
در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شدهاست. در ادامه میخواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
<mat-nav-list> <mat-list-item *ngFor="let user of users"> <a matLine [routerLink]="['/contactmanager', user.id]"> <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }} </a> </mat-list-item> </mat-nav-list>
const routes: Routes = [ { path: "", component: ContactManagerAppComponent, children: [ { path: ":id", component: MainContentComponent }, { path: "", component: MainContentComponent } ] }, { path: "**", redirectTo: "" } ];
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کردهایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل میکنیم:
import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-main-content", templateUrl: "./main-content.component.html", styleUrls: ["./main-content.component.css"] }) export class MainContentComponent implements OnInit { user: User; constructor(private route: ActivatedRoute, private userService: UserService) { } ngOnInit() { this.route.params.subscribe(params => { this.user = null; const id = params["id"]; if (!id) { return; } this.userService.getUserIncludeNotes(id) .subscribe(data => this.user = data); }); } }
اکنون میتوان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user"> <mat-spinner></mat-spinner> </div> <div *ngIf="user"> <mat-card> <mat-card-header> <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon> <mat-card-title> <h2>{{ user.name }}</h2> </mat-card-title> <mat-card-subtitle> Birthday {{ user.birthDate | date:'d LLLL' }} </mat-card-subtitle> </mat-card-header> <mat-card-content> <mat-tab-group> <mat-tab label="Bio"> <p> {{user.bio}} </p> </mat-tab> <!-- <mat-tab label="Notes"></mat-tab> --> </mat-tab-group> </mat-card-content> </mat-card> </div>
همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کردهایم (اگر دقت کنید، هر کامپوننت آن سه برگهی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره میتوان با کامپوننتهای این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا میکنیم که مناسب کار ما باشد. سپس سورس آنرا از همانجا کپی و در برنامه قرار میدهیم و در آخر آنرا بر اساس اطلاعات خود سفارشی سازی میکنیم.
نمایش جزئیات اولین کاربر در حین بارگذاری اولیهی برنامه
تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماسها ظاهر میشود و همانطور باقی میماند، با اینکه هنوز موردی انتخاب نشدهاست. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظهی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش میدهیم:
import { Router } from "@angular/router"; @Component() export class SidenavComponent implements OnInit, OnDestroy { users: User[] = []; constructor(private userService: UserService, private router: Router) { } ngOnInit() { this.userService.getAllUsersIncludeNotes() .subscribe(data => { this.users = data; if (data && data.length > 0 && !this.router.navigated) { this.router.navigate(["/contactmanager", data[0].id]); } }); } }
البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id']; if (!id) id = 1;
بستن خودکار sidenav در حالت نمایش موبایل
اگر اندازهی صفحهی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتمهای آنرا انتخاب کنیم، هرچند آنها نمایش داده میشوند، اما زیر این sidenav مخفی باقی خواهند ماند:
بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینههای آن، این قسمت بسته شده و ناحیهی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شدهاست:
<mat-sidenav #sidenav
import { MatSidenav } from "@angular/material"; @Component() export class SidenavComponent implements OnInit, OnDestroy { @ViewChild(MatSidenav) sidenav: MatSidenav;
ngOnInit() { this.router.events.subscribe(() => { if (this.isScreenSmall) { this.sidenav.close(); } }); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
- بررسی ویجت 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(""); }
حاصل کار بصورت تصویر زیر نمایش داده شده است:
This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread
Adding to an ObservableCollection from a background thread
مشکل!
اگر همین برنامه را که برای دات نت 4 کامپایل شدهاست، بر روی سیستمی که دات نت 4.5 بر روی آن نصب است اجرا کنیم، برنامه با خطای ذیل متوقف میشود:
System.InvalidOperationException: This exception was thrown because the generator for control 'System.Windows.Controls.ListView Items.Count:62' with name '(unnamed)' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected: Accumulated count 61 is different from actual count 62.
مشکل از کجاست؟
در دات نت 4 و نیم، دیگر نیازی به استفاده از کلاس MTObservableCollection یاد شده نیست و به صورت توکار امکان کار با Collectionها از طریق تردی دیگر میسر است. فقط برای فعال سازی آن باید نوشت:
private static object _lock = new object(); //... BindingOperations.EnableCollectionSynchronization(persons, _lock);
برای برنامهی دات نت 4 ایی که قرار است در سیستمهای مختلف اجرا شود چطور؟
در اینجا باید از Reflection کمک گرفت. اگر متد EnableCollectionSynchronization بر روی کلاس BindingOperations یافت شد، یعنی برنامهی دات نت 4، در محیط جدید در حال اجرا است:
public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject) { MethodInfo method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", new Type[] { typeof(IEnumerable), typeof(object) }); if (method != null) { method.Invoke(null, new object[] { collection, lockObject }); } }
همچنین اگر بخواهیم کلاس MTObservableCollection معرفی شده را جهت سازگاری با دات نت 4 و نیم به روز کنیم، به کلاس ذیل خواهیم رسید. این کلاس با دات نت 4 و 4.5 سازگار است و جهت کار با ObservableCollectionها از طریق تردهای مختلف تهیه شدهاست:
using System; using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Windows.Data; using System.Windows.Threading; namespace WpfAsyncCollection { public class AsyncObservableCollection<T> : ObservableCollection<T> { public override event NotifyCollectionChangedEventHandler CollectionChanged; private static object _syncLock = new object(); public AsyncObservableCollection() { enableCollectionSynchronization(this, _syncLock); } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { using (BlockReentrancy()) { var eh = CollectionChanged; if (eh == null) return; var dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList() let dpo = nh.Target as DispatcherObject where dpo != null select dpo.Dispatcher).FirstOrDefault(); if (dispatcher != null && dispatcher.CheckAccess() == false) { dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e))); } else { foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()) nh.Invoke(this, e); } } } private static void enableCollectionSynchronization(IEnumerable collection, object lockObject) { var method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", new Type[] { typeof(IEnumerable), typeof(object) }); if (method != null) { // It's .NET 4.5 method.Invoke(null, new object[] { collection, lockObject }); } } } }
آشنایی با الگوی طراحی Builder
public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>() .Build();
مدلهای برنامه
در اینجا قصد داریم لیست گروهها را به همراه محصولات مرتبط با آنها، توسط دو drop down list نمایش دهیم:
public class Category { public int CategoryId { set; get; } public string CategoryName { set; get; } [JsonIgnore] public IList<Product> Products { set; get; } } public class Product { public int ProductId { set; get; } public string ProductName { set; get; } }
منبع داده JSON سمت سرور
پس از مشخص شدن مدلهای برنامه، اکنون توسط دو اکشن متد، لیست گروهها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت میدهیم:
using System.Linq; using System.Text; using System.Web.Mvc; using KendoUI12.Models; using Newtonsoft.Json; namespace KendoUI12.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); // shows the page. } [HttpGet] public ActionResult GetCategories() { return new ContentResult { Content = JsonConvert.SerializeObject(CategoriesDataSource.Items), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } [HttpGet] public ActionResult GetProducts(int categoryId) { var products = CategoriesDataSource.Items .Where(category => category.CategoryId == categoryId) .SelectMany(category => category.Products) .ToList(); return new ContentResult { Content = JsonConvert.SerializeObject(products), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
در اینجا به عمد از JsonConvert.SerializeObject استفاده شدهاست تا ویژگی JsonIgnore کلاس گروهها، توسط کتابخانهی JSON.NET مورد استفاده قرار گیرد (ASP.NET MVC برخلاف ASP.NET Web API به صورت پیش فرض از JSON.NET استفاده نمیکند).
کدهای سمت کاربر برنامه
کدهای جاوا اسکریپتی Kendo UI را جهت تعریف دو drop down list به هم مرتبط و آبشاری، در ادامه ملاحظه میکنید:
<!--نحوهی راست به چپ سازی --> <div class="k-rtl k-header demo-section"> <label for="categories">گروهها: </label><input id="categories" style="width: 270px" /> <label for="products">محصولات: </label><input id="products" disabled="disabled" style="width: 270px" /> </div> @section JavaScript { <script type="text/javascript"> $(function () { $("#categories").kendoDropDownList({ optionLabel: "انتخاب گروه...", dataTextField: "CategoryName", dataValueField: "CategoryId", dataSource: { transport: { read: { url: "@Url.Action("GetCategories", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' } } } }); $("#products").kendoDropDownList({ autoBind: false, // won’t try and read from the DataSource when it first loads cascadeFrom: "categories", // the id of the DropDown you want to cascade from optionLabel: "انتخاب محصول...", dataTextField: "ProductName", dataValueField: "ProductId", dataSource: { // When the serverFiltering is disabled, then the combobox will not make any additional requests to the server. serverFiltering: true, // the DataSource will send filter values to the server transport: { read: { url: "@Url.Action("GetProducts", "Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', data: function () { return { categoryId: $("#categories").val() }; } } } } }); }); </script> <style scoped> .demo-section { width: 100%; height: 100px; } </style> }
سپس دراپ دوم که وابستهاست به دراپ داون اول، با این نکات طراحی شدهاست:
الف) خاصیت autoBind آن به false تنظیم شدهاست. به این ترتیب این دراپ داون در اولین بار نمایش صفحه، به سرور جهت دریافت اطلاعات مراجعه نخواهد کرد.
ب) خاصیت cascadeFrom آن به id دراپ داون اول تنظیم شدهاست.
ج) در منبع دادهی آن دو تغییر مهم وجود دارند:
- خاصیت serverFiltering به true تنظیم شدهاست. این مورد سبب خواهد شد تا آیتم گروه انتخاب شده، به سرور ارسال شود.
- خاصیت data نیز تنظیم شدهاست. این مورد پارامتر categoryId اکشن متد GetProducts را تامین میکند و مقدار آن از مقدار انتخاب شدهی دراپ داون اول دریافت میگردد.
اگر برنامه را اجرا کنیم، برای بار اول لیست گروهها دریافت خواهند شد:
سپس با انتخاب یک گروه، لیست محصولات مرتبط با آن در دراپ داون دوم ظاهر میگردند:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید.
فعال سازی عملیات CRUD در Kendo UI Grid
using System.Linq; using System.Net; using System.Net.Http; using System.Web.Mvc; using Kendo.DynamicLinq; using KendoUI06Mvc.Models; using Newtonsoft.Json; namespace KendoUI06Mvc.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); // shows the page. } [HttpDelete] public ActionResult DeleteProduct(int id) { var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id); if (item == null) return new HttpNotFoundResult(); ProductDataSource.LatestProducts.Remove(item); return Json(item); } [HttpGet] public ActionResult GetProducts() { var request = JsonConvert.DeserializeObject<DataSourceRequest>( this.Request.Url.ParseQueryString().GetKey(0) ); var list = ProductDataSource.LatestProducts; return Json(list.AsQueryable() .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter), JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult PostProduct(Product product) { if (!ModelState.IsValid) return new HttpStatusCodeResult(HttpStatusCode.BadRequest); var id = 1; var lastItem = ProductDataSource.LatestProducts.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } product.Id = id; ProductDataSource.LatestProducts.Add(product); // گرید آی دی جدید را به این صورت دریافت میکند return Json(new DataSourceResult { Data = new[] { product } }); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public ActionResult UpdateProduct(int id, Product product) { var item = ProductDataSource.LatestProducts .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return new HttpNotFoundResult(); if (!ModelState.IsValid || id != product.Id) return new HttpStatusCodeResult(HttpStatusCode.BadRequest); ProductDataSource.LatestProducts[item.Index] = product; //Return HttpStatusCode.OK return new HttpStatusCodeResult(HttpStatusCode.OK); } } }
var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "@Url.Action("GetProducts","Home")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "@Url.Action("PostProduct","Home")", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (product) { return "@Url.Action("UpdateProduct","Home")/" + product.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (product) { return "@Url.Action("DeleteProduct","Home")/" + product.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (options) { return kendo.stringify(options); } },