از آنجائیکه مدتی قسمتی از کارم مرتبط بود به طراحی ایمیلهای خودکار برای برنامههای تهیه شده (مثلا، ایمیلهای مرحله به مرحله یک گردش کاری ... اطلاع رسانیهای خودکار از وضعیت دادهها، گزارشاتی از برنامهها که به صورت خبرنامههای ایمیلی در بازههای زمانی مشخصی به اشخاص مشخص شده ارسال میشد و غیره)، لازم میدونم خلاصهای از تجربیات برخورد با کاربران را در این مورد در ادامه ذکر کنم، شاید مفید باشد.
1) حتما در انتهای ایمیل خودکار ارسالی، ساعت و تاریخ شمسی ارسال پیام را نیز ذکر کنید.
عموما از آنجائیکه سیستم استاندارد ارسال ایمیل بر اساس تاریخ میلادی است و تقریبا تمام کلاینتهای دریافت ایمیل موجود نیز توانایی شمسی سازی تاریخ دریافت و ارسال ایمیل را ندارند (مگر با یک سری افزونه و یا دستکاری در سیستم عامل که آنچنان خوشایند و مرسوم نیست)، ذکر تاریخ شمسی در انتهای پیام بسیار مفید خواهد بود و در اکثر اوقات استناد به ایمیلهای دریافت شده بر اساس تاریخ دریافت آنها است.
2) سعی کنید از بکارگیری عناوین (subject) ثابت جهت ارسال ایمیلهای خودکار پرهیز کنید.
دقیقا یادم میاد زمانیکه برای مدیر عامل شرکتی سه بار پشت سرهم ایمیلی با یک عنوان ارسال شده بود بنده را بازخواست کردند که چرا برنامهی شما ایمیل تکراری ارسال میکند!
بله، سعی میکنند محتوا را از روی عنوان ایمیل حدس بزنند و زمانیکه یک عنوان ثابت را برای ایمیلهای خودکار خود انتخاب کردید، تکراری به نظر خواهند رسید یا حتی ممکن است به اشتباه پیش از خوانده شدن حذف شوند.
برای مثال فرض کنید ایمیل ارجاع کاری را قرار است به صورت خودکار ارسال کنید. انتخاب عنوان ثابت برای مثال "ارجاع کار جدید" اشتباه است! این عنوان باید بر اساس نوع کار هر بار به صورت پویا متغیر باشد؛ مثلا: "ارجاع کار جدید: از طرف : ... ، موضوع: ... ، درجه اهمیت: ..." که این سه نقطهها باید توسط برنامه هر بار پر شوند.
3) هر چه میتوانید اطلاعات بیشتری را توسط یک ایمیل خودکار منتقل کنید.
مورد قبل را در نظر بگیرید. ذکر "ارجاع کار جدید ..." در عنوان و سپس مجددا ذکر همین عنوان به عنوان بدنهی ایمیل خودکار به زودی ایمیلهای شما را تبدیل به نوعی Spam آزار دهنده خواهد کرد. کار جدیدی ارجاع شده است؟ آیا میتوان خلاصهای از این کار را به همراه ایمیل نیز ارسال کرد تا کاربر حتما برای مشاهدهی ریز جزئیات کار به برنامه مراجعه نکند و این ایمیل واقعا ارزش مطالعه را داشته باشد و سبب تسریع در انجام کارها شود؟
برای مثال ذکر کلی این مورد که درخواست مرخصی جدیدی را باید تائید یا رد کنید، کافی نیست. ریز جزئیات مرخصی را هم به همراه ایمیل ارسال کنید.
4) ایمیل شما باید حاوی لینکی جهت باز کردن برنامهی تحت وب مرتبط نیز باشد.
کاری ارجاع شده است؟ بهتر است لینک پویایی را جهت هدایت کاربر به صفحهی مرتبط رسیدگی به همان کار ارجاعی ارسال کنید. به این صورت زحمت او را کمتر کرده و یک مرحله گزارش گیری را حذف خواهید کرد. یا حداقل یک محل مراجعهی کلی بعدی را به این صورت میتوان ارائه داد.
5) از بکارگیری قسمت from ایی مانند DoNotReply@Site.Com خودداری کنید.
کاربر دریافت کنندهی ایمیل باید بداند که در صورت وجود مشکل باید به کجا مراجعه کند؟ چه کسی این ایمیل را ارسال کرده؟
هرچند برنامه به صورت خودکار تمام قسمتهای این ایمیل ارسالی را تهیه میکند اما اگر خبرنامهی تنظیم شدهای نیست، حتما شخص ارسال کنندهای دارد. یا حداقل یک ایمیل عمومی را برای این مورد تنظیم کنید (ایمیلی که وجود خارجی داشته و هر از چندگاهی بررسی میشود).
6) رنگ زمینه و اندازهی قلم مناسبی را انتخاب کنید.
دقیقا برای هر کدام از موارد ذکر شده چندین بار مشکل داشتهام! عموما کسانی که ایمیلها را دریافت میکنند سن و سال دار هستند. بنابراین انتخاب فونت tahoma با اندازهی 8 یا pt 7 سبب توبیخ زود هنگام شما خواهد شد!
همچنین هر چه سادهتر بهتر. دقیقا مشکلات از زمانی آغاز میشوند که طرحی را انتخاب کنید یا رنگی را برای زمینه بکار ببرید. اینجا است که هر روز یک سلیقهی تحمیلی را باید پذیرا باشید.
7) دقیقا مشخص کنید که ایمیل دریافتی آیا رونوشت است یا خیر!
همان مبحث ارجاع کار را در نظر بگیرید. پس از اینکه سیستم راه اندازی شد، مدیر یکی از قسمتها چند روز بعد این درخواست را "حتما" ارسال خواهد کرد: رونوشت تمام کارهای ارجاعی به کلیه پرسنل بخش و همچنین ریز اقدامات آنها باید برای بنده نیز ارسال شود.
در اینجا تنها افزودن قسمت CC به ایمیلهای خودکار کفایت نمیکند. حتما به صورت درشت در بالای ایمیل، قبل از شروع بدنه ذکر کنید که ایمیل دریافتی یک رونوشت است. در غیر اینصورت باید پاسخگوی علت دریافت ایمیلهایی باشید که به درخواست خودشان CC شده است!
8) از ایمیلهای خودکار برنامه log تهیه کنید.
بارها به این مساله برخورد کردهام که اشخاص برای شانه خالی کردن از انجام کار محوله، سعی در تخریب کار شما خواهند داشت. خیلی ساده عنوان میکنند که ایمیلی را دریافت نکردهاند. حالا شما بیاید ثابت کنید که اگر سیستم مشکل داشت کلا برای هیچ کسی ایمیل ارسال نمیشد، نه فقط برای شما. در اینگونه مواقع وجود یک لاگ از ایمیلها (ثبت در بانک اطلاعاتی) و ارجاع به آنها بسیار راه گشا است.
9) راهی را برای خلاص شدن از شر دریافت ایمیلهای خودکار نیز پیش بینی کنید!
همان مورد 7 را در نظر بگیرید. دو روز اول خیلی ذوق خواهند کرد! روز سوم وقتی انبوهی از ایمیلها را دریافت کردند، مشکل شما هم شروع خواهد شد. بنابراین امکان تنظیم دریافت یا عدم دریافت ایمیل را حتما در برنامه قرار دهید. یا حداقل نحوهی ایجاد یک پوشه جدید و فیلتر کردن ایمیلهای رسیده و هدایت خودکار آنها به این پوشهی جدید را آموزش دهید.
خوب! حالا به نظر شما این ایمیل خودکار ارسالی سایت IDevCenter که اخیرا اضافه شده است چه نمرهای را کسب میکند؟
- تاریخ شمسی در انتهای ایمیل ندارد.
- عنوانها ثابت هستند.
- هیچ جزئیاتی ارائه نشده است.
- لینک مرتبط دارد.
- قسمت from مناسبی دارد.
- ساده است؛ خوب است! فقط اندازه قلم آن بهتر است یک شماره بزرگتر شود.
- بحث رونوشت اینجا مورد ندارد.
- بحث لاگ ... شخصی است.
- امکان تنظیم دریافت ایمیل پیش بینی شده است.
نمره از 7 : 3.5
تعداد درخواست بر اساس هر Urls
در ابتدای شروع من پروژه خودم رو Run کردم و متوجه شدم بر اساس هر Url یک درخواست به sql ارسال میشه ( در تصویر زیر مشاهده کنید ، تعداد 24 بار درخواست ارسال میشه ) :
من از این قسمت استفاده میکنم برای اعتبارسنجی درخواست ها.
آیا واقعا با توجه به برنامه شما این تعدا ددرخواست ارسال میشه یا اینکه چون من با برنامه شما آشنا نیستم این اشتباه رو کردم ؟
بررسی وبسایتهای سازمانی در ایران
افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard
در اینجا قصد داریم صفحهای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شدهی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شدهی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه میکنیم:
>ng g c Dashboard/CallProtectedApi
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component"; const routes: Routes = [ { path: "callProtectedApi", component: CallProtectedApiComponent, data: { permission: { permittedRoles: ["Admin", "User"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شدهی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active"> <a [routerLink]="['/callProtectedApi']">Call Protected Api</a> </li>
نمایش و یا مخفی کردن قسمتهای مختلف صفحه بر اساس نقشهای کاربر وارد شدهی به سیستم
در ادامه میخواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شدهی سمت سرور را بازگشت دهند. دکمهی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمهی دوم توسط کاربری با نقشهای Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر میدهیم:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-call-protected-api", templateUrl: "./call-protected-api.component.html", styleUrls: ["./call-protected-api.component.css"] }) export class CallProtectedApiComponent implements OnInit { isAdmin = false; isUser = false; result: any; constructor(private authService: AuthService) { } ngOnInit() { this.isAdmin = this.authService.isAuthUserInRole("Admin"); this.isUser = this.authService.isAuthUserInRole("User"); } callMyProtectedAdminApiController() { } callMyProtectedApiController() { } }
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()"> Call Protected Admin API [Authorize(Roles = "Admin")] </button> <button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()"> Call Protected API ([Authorize]) </button> <div *ngIf="result"> <pre>{{result | json}}</pre> </div>
دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال میکند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل میتوان در AuthService تولید نمود:
getBearerAuthHeader(): HttpHeaders { return new HttpHeaders({ "Content-Type": "application/json", "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}` }); }
روش دوم انجام اینکار که مرسومتر است، اضافه کردن خودکار این هدر به تمام درخواستهای ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل میکنیم:
import { Injectable } from "@angular/core"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import { AuthService, AuthTokenType } from "./auth.service"; @Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private authService: AuthService) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام میشود:
import { HTTP_INTERCEPTORS } from "@angular/common/http"; import { AuthInterceptor } from "./services/auth.interceptor"; @NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true } ] }) export class CoreModule {}
در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعهدهندههای مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors: Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
import { Injector } from "@angular/core"; constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService);
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private injector: Injector) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); } return next.handle(request); } }
تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شدهی سمت سرور
اکنون پس از افزودن AuthInterceptor، میتوان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویسهای Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازندهی CallProtectedApiComponent تزریق میکنیم:
constructor( private authService: AuthService, private httpClient: HttpClient, @Inject(APP_CONFIG) private appConfig: IAppConfig, ) { }
callMyProtectedAdminApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); } callMyProtectedApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:
مدیریت خودکار خطاهای عدم دسترسی ارسال شدهی از سمت سرور
ممکن است کاربری درخواستی را به منبع محافظت شدهای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده میتوان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحهی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); });
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor( private injector: Injector, private router: Router) { } intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { const authService = this.injector.get(AuthService); const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken); if (accessToken) { request = request.clone({ headers: request.headers.set("Authorization", `Bearer ${accessToken}`) }); return next.handle(request) .catch((error: any, caught: Observable<HttpEvent<any>>) => { if (error.status === 401 || error.status === 403) { this.router.navigate(["/accessDenied"]); } return Observable.throw(error); }); } else { // login page return next.handle(request); } } }
using ASPNETCore2JwtAuthentication.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Mvc; namespace ASPNETCore2JwtAuthentication.WebApp.Controllers { [Route("api/[controller]")] [EnableCors("CorsPolicy")] [Authorize(Policy = CustomRoles.Editor)] public class MyProtectedEditorsApiController : Controller { public IActionResult Get() { return Ok(new { Id = 1, Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]", Username = this.User.Identity.Name }); } } }
callMyProtectedEditorsApiController() { this.httpClient .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`) .map(response => response || {}) .catch((error: HttpErrorResponse) => Observable.throw(error)) .subscribe(result => { this.result = result; }); }
نکتهی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور
اگر برنامهی سمت سرور ما که توکنها را اعتبارسنجی میکند، ریاستارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکنهای فعلی او برگشت خواهند خورد و از مرحلهی تعیین اعتبار رد نمیشوند. در این حالت کاربر خطای 401 را دریافت میکند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکیهای برنامههای ASP.NET Core هاست شدهی در IIS پس از ریاستارت آن» را فراموش نکنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
Media Type یا MIME Type نشان دهنده فرمت یک مجموعه داده است. در HTTP، مدیا تایپ بیان کننده فرمت message body یک درخواست / پاسخ است و به دریافت کننده اعلام میکند که چطور باید پیام را بخواند. محل استاندارد تعیین Mime Type در هدر Content-Type است. درخواست کننده میتواند با استفاده از هدر Accept لیستی از MimeTypeهای قابل قبول را به عنوان پاسخ، به سرور اعلام کند.
Asp.net Web API از MimeType برای تعیین نحوه serialize یا deserialize کردن محتوای دریافتی / ارسالی استفاده میکند
MediaTypeFormatter
Web API برای خواندن/درج پیام در بدنه درخواست/پاسخ از MediaTypeFormmaterها استفاده میکند. اینها کلاسهایی هستند که نحوهی Serialize کردن و deserialize کردن اطلاعات به فرمتهای خاص را تعیین میکنند. Web API به صورت توکار دارای formatter هایی برای نوعهای XML ، JSON، BSON و Form-UrlEncoded میباشد. همه اینها کلاس پایه MediaTypeFormatter را پیاده سازی میکنند.
مسئله
یک پروژه Web API بسازید و view model زیر را در آن تعریف کنید:
public class NewProduct { [Required] public string Name { get; set; } public double Price { get; set; } public byte[] Pic { get; set; } }
همانطور که میبینید یک فیلد از نوع byte[] برای تصویر محصول در نظر گرفته شده است.
حالا یک کنترلر API ساخته و اکشنی برای دریافت اطلاعات محصول جدید از کاربر مینویسیم :public class ProductsController : ApiController { [HttpPost] public HttpResponseMessage PostProduct(NewProduct model) { if (ModelState.IsValid) { // ثبت محصول return new HttpResponseMessage(HttpStatusCode.Created); } return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState); } }
و یک صفحه html به نام index.html که حاوی یک فرم برای ارسال اطلاعات باشد :
<!DOCTYPE html> <html> <head> <title></title> </head> <body> <h1>ساخت MediaTypeFormatter برای Multipart/form-data</h1> <h2>محصول جدید</h2> <form id="newProduct" method="post" action="/api/products" enctype="multipart/form-data"> <div> <label for="name">نام محصول : </label> <input type="text" id="name" name="name" /> </div> <div> <label for="price">قیمت : </label> <input type="number" id="price" name="price" /> </div> <div> <label for="pic">تصویر : </label> <input type="file" id="pic" name="pic" /> </div> <div> <button type="submit">ثبت</button> </div> </form> </body> </html>
زمانی که فرم حاوی فایلی برای آپلود باشد مشخصه encType باید برابر با Multipart/form-data مقداردهی شود تا اطلاعات فایل به درستی کد شوند. در زمان ارسال فرم Content-type درخواست برابر با Multipart/form-data و فرمت اطلاعات درخواست ارسالی به شکل زیر خواهد بود :
همانطور که میبینید هر فیلد در فرم، در یک بخش جداگانه قرار گرفته است که با خط چین هایی از هم جدا شده اند. هر بخش، headerهای جداگانه خود را دارد.
- Content-Disposition که نام فیلد و نام فایل را شامل میشود .
- content-type که mime type مخصوص آن بخش از دادهها را مشخص میکند.
پس از اینکه فرم را تکمیل کرده و ارسال کنید ، با پیام خطای زیر مواجه میشوید :
خطای روی داده اعلام میکند که Web API فاقد MediaTypeFormatter برای خواندن اطلاعات ارسال شده با فرمتMultiPart/Form-data است. Web API برای خواندن و بایند کردن پارامترهای complex Type از درون بدنه پیام یک درخواست از MediaTypeFormatter استفاده میکند و همانطور که گفته شد Web API فاقد Formatter توکار برای deserialize کردن دادههای با فرمت Multipart/form-data است.
راه حلها :روشی که در سایت asp.net برای آپلود فایل در web api استفاده شده، عدم استفاده از پارامترها و خواندن محتوای Request در درون کنترلر است. که به طبع در صورتی که بخواهیم کنترلرهای تمیز و کوچکی داشته باشیم روش مناسبی نیست. از طرفی امتیاز parameter binding و modelstate را هم از دست خواهیم داد.
روش دیگری که میخواهیم در اینجا پیاده سازی کنیم ساختن یک MediaTypeFormatter برای خواندن فرمت Multipart/form-data است. با این روش کد موردنیاز کپسوله شده و امکان استفاده از binding و modelstate را خواهیم داشت.
برای ساختن یک MediaTypeFormatter یکی از 2 کلاس MediaTypeFormatter یا BufferedMediaTypeFormatter را باید پیاده سازی کنیم . تفاوت این دو در این است که BufferedMediaTypeFormatter برخلاف MediaTypeFormatter از متدهای synchronous استفاده میکند.
یک کلاس به نام MultiPartMediaTypeFormatter میسازیم و کلاس MediaTypeFormatter را به عنوان کلاس پایه آن قرار میدهیم .
public class MultiPartMediaTypeFormatter : MediaTypeFormatter { ... }
ابتدا در تابع سازنده کلاس فرمت هایی که میخواهیم توسط این کلاس خوانده شوند را تعریف میکنیم :
public MultiPartMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue("multipart/form-data")); }
سپس با پیاده سازی توابع CanReadType و CanWriteType مربوط به کلاس MediaTypeFormatter مشخص میکنیم که چه مدلهایی را میتوان توسط این کلاس serialize / deserialize کرد. در اینجا چون میخواهیم این کلاس محدود به یک مدل خاص نباشد، از یک اینترفیس برای شناسایی کلاسهای مجاز استفاده میکنیم .
public interface INeedMultiPartMediaTypeFormatter { }
و آنرا به کلاس newProduct اضافه میکنیم :
public class NewProduct : INeedMultiPartMediaTypeFormatter { ... }
public override bool CanReadType(Type type) { return typeof(INeedMultiPartMediaTypeFormatter).IsAssignableFrom(type); } public override bool CanWriteType(Type type) { return false; }
و اما تابع ReadFromStreamAsync که کار خواندن محتوای ارسال شده و بایند کردن آنها به پارامترها را برعهده دارد
public async override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContent content, IFormatterLogger formatterLogger)
ابتدا محتوای ارسال شده را خوانده و اطلاعات فرم را استخراج میکنیم و از طرف دیگر با استفاده از کلاس Activator یک نمونه از مدل جاری را ساخته و لیست propertyهای آنرا استخراج میکنیم.
MultipartMemoryStreamProvider provider = await content.ReadAsMultipartAsync(); IEnumerable<HttpContent> formData = provider.Contents.AsEnumerable(); var modelInstance = Activator.CreateInstance(type); IEnumerable<PropertyInfo> properties = type.GetProperties();
سپس در یک حلقه به ترتیب برای هر property متعلق به مدل، در میان اطلاعات فرم جستجو میکنیم. برای پیدا کردن اطلاعات متناظر با هر property در هدر Content-Disposition که در بالا توضیح داده شد، به دنبال فیلد همنام با property میگردیم .
foreach (PropertyInfo prop in properties) { var propName = prop.Name.ToLower(); var propType = prop.PropertyType; var data = formData.FirstOrDefault(d => d.Headers.ContentDisposition.Name.ToLower().Contains(propName));
گفتیم که هر فیلد یک هدر، Content-Type هم میتواند داشته باشد. این هدر به صورت پیش فرض معادل text/plain است و برای فیلدهای عادی قرار داده نمیشود . در این مثال چون فقط یک
فیلد غیر رشته ای داریم فرض را بر این گرفته ایم که در صورت وجود Content-Type، فیلد مربوط به تصویر است. در صورتیکهContentType وجود داشته باشد، محتوای فیلد را به شکل Stream
خوانده به byte[] تبدیل و با استفاده از متد SetValue در property مربوطه قرار میدهیم.
if (data != null) { if (data.Headers.ContentType != null) { using (var fileStream = await data.ReadAsStreamAsync()) { using (MemoryStream ms = new MemoryStream()) { fileStream.CopyTo(ms); prop.SetValue(modelInstance, ms.ToArray()); } } }
در صورتی که Content-Type غایب باشد بدین معنی است که محتوای فیلد از نوع رشته است ( عدد ، تاریخ ، guid ، رشته ) و باید به نوع مناسب تبدیل شود. ابتدا آن را به صورت یک رشته میخوانیم و با استفاده از Convert.ChangeType آنرا به نوع مناسب تبدیل میکنیم و در property متناظر قرار میدهیم .
if (data != null) { if (data.Headers.ContentType != null) { //... } else { string rawVal = await data.ReadAsStringAsync(); object val = Convert.ChangeType(rawVal, propType); prop.SetValue(modelInstance, val); } }
return modelInstance;
config.Formatters.Add(new MultiPartMediaTypeFormatter());
ارتقاء به HTTP Client در Angular 4.3
Unexpected token o in JSON at position 1 at JSON.parse
برای طراحی گزارش شما میتوانید به سه روش این کار را انجام دهید.
1- طراحی در برنامه طراح گزارش
2- طراحی از داخل ویژوال استودیو
3- طراحی گزارش در زمان اجرا
برای شروع شما میتوانید نسخه آزمایشی این گزارشساز را دریافت کنید. تنها محدودیت این نسخه نمایش عبارت Demo در چاپ میباشد.
برنامه Designer را اجرا کنید. در صورتی که برای اولین بار است این برنامه را اجرا میکنید ابتدا باید رابط کاربری خود را انتخاب نمایید. نوار ابزار سمت چپ تمامی ابزارهای پرکاربرد طراحی گزارش را در اختیارتان قرار میدهد. ابزارهایی که در این بخش درباره آنها توضیح داده خواهد شد عبارتند از:
Header, Footer, Data, Page Header, Page Footer, Report Title, Report Summery
*به ابزارهای بالا Band گفته میشود.
Header , Footer :
همانطور که از نامشان پیداست در قسمت بالا و پایین بخشی از گزارش قرار میگیرند که برای استفاده در بالا و پایین بند Data میباشد. به عنوان مثال بند Header مناسب طراحی سرستونهای یک جدول میباشد و بند Footer هم جهت نمایش اطلاعات انتهایی یک جدول. ولی شما میتوانید با تنظیم خصوصیات هر بند رفتار و نمایش آنها را به طور کل تغییر دهید. نکته مثبت این گزارشساز این است که شما میتوانید بیش از یک واحد از هر بند را بر روی صفحه طراح خود قرار دهید، به عنوان مثال شما میتوانید دو بند Header داشته باشید که یکی در صفحات زوج و دیگری در صفحات فرد نمایش داده شود.
Data :
این بند جهت نمایش اطلاعات از منبع دادهها میباشد. به این معنا که به ازای هر سطر از دادهها یک بار این بخش نمایش داده میشود. تعداد دفعات نمایش این بند محدود به تعداد سطرهای منبع داده و یا اندازه صفحه و همچنین خصوصیت محدوده نمایش سطرها در یک صفحه میباشد.
Page Header , Page Footer :
این دو بند با توجه به نامشان جهت نمایش در بالا و پایین هر صفحه از گزارش میباشد. البته باز هم یادآور میشوم که با تغییر در خصوصیاتشان میتوانید رفتار و نحوه نمایش آنها را تغییر دهید.
Report Title :
این بند فقط در ابتدای گزارش نمایش داده خواهد شد.
Report Summery :
این بند بلافاصله بعد از اتمام گزارش نمایش داده خواهد شد.
مثال :
برای شروع در Designer یک گزارش جدید از نوع Blank Report ایجاد نمایید. سپس در پنل Dictionary بر روی New Item کلیک کرده و گزینه XML Data را انتخاب نمایید. با توجه به محل نصب گزارشساز وارد مسیر …\Bin\Data شده و فایلهای Demo.xsd و Demo.xml را برای قسمتهای مربوطه انتخاب نمایید. یک بار دیگر بر رو New Item کلیک کرده و گزینه New Data Source را انتخاب نمایید، از لیست ظاهر شده کانکشنی را که ایجاد کردهاید را انتخاب نمایید؛ نتیجه کار تا اینجا باید به صورت زبر باشد.
جدول Product را دراگ کرده و بر روی صفحه طراحی گزارش رها کنید. فرم Data ظاهر میشود این فرم را مطابق تصویر زیر تنظیم نمایید.
حال بر روی صفحه طراحی گزارش بندهای Header, Data, Footer مشاهده میشود؛ حال شما میتوانید با کلیک بر روی سربرگ Preview خروجی گزارش را ببینید.
توابع :
این گزارشساز دارای توابع بسیاری است که اکثر نیازهای شما را برطرف میکند به عنوان مثال تابع تبدیل عدد به حروف به زبان فارسی. همچنین شما میتوانید توابع خاص خود را ساخته و به صورت رفرنس به گزارش اضافه نمایید.
در این بخش ما از توابع موجود در گزارش استفاده خواهیم کرد. برای شروع بر روی کامپوننت Text در بند Footer زیر ستون UnitPrice دابل کلیک کرده تا فرم TextEditor ظاهر شود. سربرگ Summery را انتخاب نمایید. مطابق اطلاعات زیر بخشها را تنظیم نمایید.
Summery Function: Sum
Data Band: DataProducts
Data Column: Products.UnitPrice
حال بر روی سربرگ Preview
کلیک نمایید تا خروجی گزارش را ببینید. جمع ستون UnitPrice
فقط در صفحه آخر نمایش داده خواهد شد. اگر بخواهید جمع ستون در پایین هر صفحه
نمایش داده شود ابتدا باید خصوصیت Print on All Pages بند Footer به True
ست شود. سپس بر روی کامپوننت Text در بند Footer،
دابل کلیک نمایید و در فرم TextEditor
سربرگ Summery
تیک Running Total
را به حالت انتخاب شده در بیاورید، حال خروجی گزارش را ببینید، جمع در انتهای هر
صفحه ظاهر میشود.
متغیرها :
در این گزارش ساز دو نوع متغیر وجود دارد؛ نرمال و سیستمی. نوع سیستمی شامل متغیرهایی میشود که کاربرد مشخصی در تهیه گزارش دارند، مثل شماره صفحه، شماره ردیف، عنوان گزارش و ...
برای مثال شما میتوانید متغیر سیستمی Line را برای روی صفحه طراحی دراگ کنید. دو کامپوننت Text بر روی صفحه ایجاد میشود. اولی با محتوای Line و دومی با محتوای {Line}. اولی را به بند Header و دومی را به بند Data منتقل کنید و سپس خروجی گزارش را مشاهده نمایید، حال گزارش شما دارای شماره ردیف است.
متغیرهای نرمال تقریبا همانند متغیرهایی هستند که همه روزه شما در برنامههای خود از آنها استفاده میکنید. با کلیک بر روی New Item گزینه New Variable را انتخاب نمایید و نوع متغیر را Decimal انتخاب نمایید، سپس متغیر ایجاد شده را دراگ کرده و بروی صفحه طراحی قرار دهید و مشابه متغیر Line عمل کرده و کامپوننتهای Text را در بندهای مناسب قرار دهید. سپس بند Data بر روی صفحه طراحی را انتخاب نمایید، در پنل Properties بر روی Eventes کلیک کرده سپس در رویداد Rendering کد زیر را وارد نمایید.
Variable1 += Products.UnitPrice
حال در خروجی گزارش میتوانید مقادیر محاسبه
شده را ببینید. توجه داشته باشید که شما میتوانید در رویدادهای این گزارشساز به
زبان VB
و C#
برنامه نویسی کنید و محدود به یک خط کد نمیباشید.
شما میتوانید گزارش ساخته شده را به صورتهای مختلف ذخیره کنید از جمله کد C# و یا یک اپلیکیشن قابل اجرا.
دارا بودن امکانات بسیار قدرتمند و پشتیبانی از محیط فارسی و همچنین پشتیبانی آنها جهت پاسخگویی به سوالات، چه از طریق ایمیل یا چت، از نقاط قوت این ابزار به شمار میروند. در جدول مقایسات میتوانید تفاوت نسخههای موجود این گزارش ساز را مشاهده کنید. برای استفاده در MVC از نسخه وب آن استفاده میکنیم.
در این مقاله قصد داریم با نحوه راه ندازی این ابزار در وب (MVC) آشنا شویم که شامل مباحث زیر میشود:
- استفاده از EF به عنوان منبع داده و ارسال آنها به سمت گزارش ساز
- نحوه طراحی فایل MRT و بایند کردن دادههای اطلاعاتی و ایجاد جدول
- استفاده از امکانات فایل خروجی ، چاپ و پیش نمایش و...
- بررسی Direction جهت استفاده در محیطهای فارسی زبان
- نحوه ارسال اطلاعات بین دو اکشن متفاوت
طراحی فایل MRT
فایل MRT در واقع یک قالب (Template) خالی از مقادیر متغیر است که در StimulSoft Studio به طراحی آن میپردازیم و در برنامه خود، این مقادیر متغیر را با اطلاعات دلخواه خود جایگزین میکنیم. تصویر زیر یک نمونه از یک گزارش خالی است که ابتدا آن را طراحی کرده و سپس در برنامه آن را مورد استفاده قرار میدهیم:
برای اینکه فایل MRT بتواند دیتاهای لازمی را که به آن پاس میدهیم، بخواند و در جای مشخص شده قرار بدهد، باید یک BussinessObject برای آن ایجاد کنیم. بعد از اینکه یک گزارش جدید ایجاد کردید، در سمت راست به قسمت Dictionary بروید و در قسمت BussinessObject گزینه NewBussinessObject را انتخاب کنید. یک نام و نام مستعار که عموما هم یکی است، برای آن انتخاب کنید. در زیر همان پنجره شما میتوانید ستونهای اطلاعاتی خود را تعریف کنید. در اینجا من میخواهم اطلاعات یک راننده را به همراه خودروی وی، نشان دهم. برای همین، من دو موجودیت راننده و خودروی راننده را دارم. پس اسم Business Object را DriverReport میگذارم و ستونهای اطلاعاتی فقط راننده (بدون در نظر گرفتن خودروی وی) را وارد میکنم.
در همین کادر بالا شما میتوانید تصیم بگیرید که آیا میخواهید اطلاعات خودرو را به همراه دیگرستونهای اطلاعاتی راننده، ایجاد کنید یا اینکه برای خودرو یک نوع مجزا انتخاب کنید. اگر تنها یک خودرو برای راننده باشد، شاید راحتتر باشید همانند اطلاعات راننده با آن رفتار کنید. ولی اگر مثلا بخواهید خودرویهای گذشته راننده را هم جز لیست داشته باشید، بهتر است یک Business Object جدید متعلق و زیر مجموعه Business Object راننده ایجاد کنید. در اینجا چون تنها یک خودرو است، من آن اطلاعات آن را به همراه راننده، ارسال میکنم. شکل زیر ساختار درختی از گزارش بالاست:
شکل زیر هم یک ساختار دیگر از یک گزارش است که شامل Business objectهای مختلف میشود:
سپس همین فیلدها را به سمت صفحه خالی بکشانید. با دو بار کلیک روی فیلدهای قرار گرفته در صفحه، با نحوه بایند کردن مقادیر آشنا میشوید؛ هر فیلدی که قرار است دیتای آن بایند شود، باید به شکل زیر در بخش Expression پنجره باز شده، نوشته شود:
{driverReport.LastName}
در دیکشنری همچنین انواع دیگری از فیلدها نیز به چشم میخورد:
متغیرها: این نوع فیلد یک متغیر است که به طور جداگانه میتواند مقداردهی شود و از آن بیشتر برای ارسال دادههای تکی چون تصاویر، تاریخ شمسی و ... میتوان استفاده کرد.
متغیرهای سیستمی: این نوع متغیرها توسط خود گزارش ساز به طور مستقیم پر میشوند که شامل شماره صفحه، تاریخ و زمان، تعداد صفحات، مقادیر دو ارزشی (آیا صفحه آخر گزارش است؟) و ... میشود.
توابع: گزارش ساز شامل یک سری توابع آماده برای اعمال تغییرات بر روی دادهها میباشد که در دستههای مختلفی چون کار با رشتهها، زمان، ریاضیات و... قرار گرفتهاند.
بعد از تکمیل آن، فایل MRT را ذخیره و در یک دایرکتوری در ساختار پروژه قرار دهید.
راه اندازی گزارش ساز در ASP.Net MVC
اولین کاری که میکنیم، ورود سه dll اصلی به پروژه است:
Stimulate.Base
Stimulate.Report
Stimulate.Report.MVC
در مرحله بعد یک متد ساخته و یک ویوو را برای صفحه گزارش گیری ایجاد میکنیم:
public ActionResult Report(int id) { return View(); }
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions() { Localization = "~/content/reports/fa.xml", Actions = { GetReportSnapshot = "LoadReportSnapshot", ViewerEvent = "ViewerEvent", ExportReport = "ExportReport", PrintReport = "PrintReport", } }
در نسخههای دو سال اخیر، استفاده از این Helper تفاوتهایی در نحوه استفاده از خصوصیتهای آن کرده است. در این روش جدید، پراپرتیها دسته بندی شده و برای دسترسی به هر کدام باید به بخش آن مراجعه کنید؛ مثلا پراپرتیهای Action، در دسته Actions قرار گرفتهاند یا خصوصیتهای ظاهری در دسته Appearance، یا گزینههای مرتبط با خروجی گرفتنها، در دسته Export قرار گرفتهاند و الی آخر که در نسخههای پیشین، کد بالا را به شکل زیر، با پیشوند نام دسته مینوشتیم:
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions() { Localization = "~/content/reports/fa.xml", ActionGetReportSnapshot = "LoadReportSnapshot", ActionViewerEvent = "ViewerEvent", ActionExportReport = "ExportReport", ActionPrintReport = "PrintReport", }
بعد از آن لازم است دیتاها را از طریق EF خوانده و به یک مدل جدید که بر اساس اطلاعات گزارش شماست و قرار است گزارش شما این پراپرتیها را بشناسد، به طور دستی یا با استفاده یک کتابخانه mapping مثل automapper انتقال دهید. یا حتی میتوانید مانند کد زیر از ساختاری ناشناس استفاده کنید. در کد زیر، من به صورت تمرینی اطلاعات یک راننده و خودروی او را انتقال میدهم:
var driver = new { FirstName = "علی", LastName = "یگانه مقدم", NationalCode = "12500000000", FatherName = "حسین", Model = "نام خودرو", MotorNumber = 415244, ProductionYear = 1394, Capacity = 4 };
var driver = new { FirstName = "علی", LastName = "یگانه مقدم", NationalCode = "12500000000", FatherName = "حسین", car = new { Model = "نام خودرو", MotorNumber = 415244, ProductionYear = 1394, Capacity = 4 } };
var report = new StiReport(); report.Load(Server.MapPath("~/Content/Reports/driver.mrt")); report.RegBusinessObject("driverReport", driver); report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
var report = new StiReport(); report.RegBusinessObject("driverReport", driver); report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date)); report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
پس کد کامل ما برای ایجاد یک گزارش به شکل زیر میشود:
public ActionResult LoadReportSnapshot() { var driver = new { FirstName = "علی", LastName = "یگانه مقدم", NationalCode = "12500000000", FatherName = "حسین", Model = "نام خودرو", MotorNumber = 415244, ProductionYear = 1394, Capacity = 4 }; var report = new StiReport(); report.Load(Server.MapPath("~/Content/Reports/driver.mrt")); report.RegBusinessObject("driverReport", driver); report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date)); return StiMvcViewer.GetReportSnapshotResult(HttpContext, report); }
اگر دوباره در ویو مربوطه، به سراغ helper برویم میبینیم که سه اکشن متد دیگر وجود دارند که در زیر، به ترتیب با نحوه کار آنها و کد اکشن متد آنها اشاره میکنیم:
Viewer Events : این اکشن متد که تنها یک خط ActionResult استاتیک را فراخوانی میکند، جهت مدیریت رویدادهای گزارش چون: زوم، صفحه بندی گزارش، خروجیها و چاپ میباشد و وجود آن در گزارش از الزامات است.
public virtual ActionResult ViewerEvent() { return StiMvcViewer.ViewerEventResult(); }
PrintReport: برای مدیریت و ارسال گزارشات به دستگاه چاپ میباشد. این اطلاعات از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط PrintReportResult آن را دریافت میکند.
public virtual ActionResult PrintReport() { return StiMvcViewer.PrintReportResult(this.HttpContext); }
ExportReport: گزارش ساز استیمول به شما اجاز میدهد در فرمتهای گوناگونی چون xlsx,docx,pptx,pdf,rtf و ... از گزارش خود خروجی بگیرید. اطلاعات گزارش از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط ExportReportResult دریافت میشود.
public virtual ActionResult ExportReport() { return StiMvcViewer.ExportReportResult(this.HttpContext); }
البته خوشبختانه این مشکل در حالت پیش نمایش و چاپ و خروجیها دیده نمیشود و فقط مختص نمایش روی فرم Html است. برای حل این مشکل ممکن است از گزینه یا پراپرتی RightToLeft، در بخش Appearance موجود در helper استفاده کنید که البته استفاده از آن مانند تصویر بالا، فقط محدود به container گزارش و نوار ابزار آن میشود. برای حل این مشکل کافی است کد css زیر را به صفحه گزارش اضافه کنید تا مشکل حل شود:
.stiMvcViewerReportPanel table{ direction:ltr !important; }
حال حتما پیش خود میگویید که این روش برای اطلاعات ایستا و تمرینی مناسب است و من چگونه باید پارامترهای ارسالی به اکشن متد Report را به اکشن متد LoadReportSnapshot ارسال کنم. برای این منظور استفاده از SessionStateها زیاد توصیه شدهاست:
public virtual ActionResult Report(int id) { TempData["id"]=id; return View(); } public virtual ActionResult LoadReportSnapshot() { var driverId = (int)TempData ["id"]; //..... }
public virtual ActionResult Report(int id) { return View(); } public virtual ActionResult LoadReportSnapshot(int id) { //..... }
نکته بسیار مهم: گزارش ساز استیمول متاسفانه شامل تنظیم پیش فرض نامناسبی است که عملیات کش را بر روی گزارشها اعمال میکند. به عنوان مثال تصور کنید من صفحه گزارش شخصی به نام «وحید نصیری» را باز میکنم و در تب دیگر گزارش شخص دیگری با نام «علی یگانه مقدم» را باز میکنم. حال اگر کاربر به سراغ تب آقای نصیری برود و بخواهد چاپ یا خروجی درخواست کند، اشتباها با گزارش علی یگانه مقدم روبرو خواهد شد که این اتفاق به دلیل کش شدن رخ میدهد. برای غیر فعال کردن این قابلیت پیش فرض، کد زیر را در Helper اضافه کنید:
Server = { GlobalReportCache = false }