سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا
آشنایی با Flex Layout Box Model
برای طراحی ظاهر یک برنامهی وب نیاز است عناصر آنرا در مکانهای مختلفی از صفحه قرار داد که به آن Layout گفته میشود. برای این منظور عموما 4 روش ذیل مرسوم هستند:
1. Table
2. Float, position, clear
3. CSS Grids
4. FlexBox CSS
امروزه دیگر آنچنان روشهای 1 و 2 به صورت مستقیم مورد استفاده قرار نمیگیرند. CSS Grid روش نهایی طراحی Layout در آینده خواهد بود و در حال حاضر تعداد مرورگرهایی که از آن پشتیبانی میکنند، قابل توجه نیست؛ اما از FlexBox در IE 11، کروم 21 و فایرفاکس 22 به بعد پشتیبانی میشود.
FlexBox CSS، سیلان المانهای قرار گرفتهی در داخل آنرا سبب میشود. در اینجا یک container اصلی وجود دارد که در برگیرندهی المانها است. در تصویر فوق دو محور را مشاهده میکنید. محور افقی از چپ به راست ادامه پیدا میکند. محور عمودی نحوهی ارتباط عناصر را مشخص میکند.
اکنون این سؤال مطرح میشود که چه تفاوتی بین یک Grid و FlexBox CSS وجود دارد؟ در یک Grid طراحی دو بعدی سطرها و ستون وجود دارد. اما به FlexBox باید به صورت سیلان یک بعدی سلولها نگاه کرد. برای مثال عناصر قرار گرفتهی درون Container یا به صورت افقی درون آن گسترده شده و قرار میگیرند و یا به صورت عمودی.
نحوهی تفکر و کارکرد با FlexBox چگونه است؟
در اینجا باید به دو مفهوم دقت داشت:
الف) سیلان عناصر درون Container که میتواند افقی و یا عمودی باشد.
ب) اندازهی المانها که میتواند ثابت و یا نسبی باشد.
یک Container، جهت سیلان عناصر درون آنرا مشخص میکند. المانهای آن، اندازه، فاصلهی از یکدیگر و ترتیب قرارگیری را ارائه میدهند. یک flex container میتواند شامل چندین flex container تو در تو نیز باشد.
نحوهی سیلان عناصر در FlexBox چگونه است؟
برای نمونه طرحبندی متداول ذیل را درنظر بگیرید:
نحوه تفکر در مورد طراحی این طرحبندی، باید از بیرون به درون و از بالا به پایین (سیلان عمودی) باشد:
سپس نحوهی سیلان عناصر درون Containerهای تعریف شده را مشخص میکنیم. برای مثال اولین Container دارای سیلان افقی از چپ به راست خواهد بود که عنصر سوم آن به دلیل اندازههای مشخص دو عنصر قبلی، به سطر دوم منتقل شدهاست.
در ادامه به قسمت میانی میرسیم که آن نیز دارای سیلان افقی از چپ به راست است:
در اینجا نیز میتوان سه Container را متصور شد که وسطی دارای سیلان افقی از راست به چپ است و مواردی که بر اساس اندازهی آنها در یک سطر جا نشدهاند، به سطر بعدی منتقل خواهند شد:
و تمام این سیلانها و انتقال به سطرهای بعدی بر اساس اندازهی المانها صورت میگیرد:
البته در این تصویر یک ایراد هم وجود دارد. با توجه به اینکه در ناحیهی میانی سه Container تعریف خواهند شد. Container ایی که در میان آن قرار میگیرد، دارای سیلان خاص خودش است و اندازههای آن باید نسبت به این Container تعریف شوند و نه نسبت به کل ناحیهی میانی. یعنی بجای اینکه 50 درصد، 25، 25 و 50 درصد را داشته باشیم، اینها در اصل 100 درصد، 50 و 50 درصد و سپس 100 درصد هستند.
معرفی کتابخانهی Angular Flex Layout
برای کار با Flex CSS نیاز است:
- مقدار زیادی کد CSS نوشت.
- نیاز به درک عمیقی از Flex Box دارد.
- نیاز است با باگهای مرورگرها و تفاوتهای پیاده سازیهای آنها در مورد FlexBox آشنا بود.
- نیاز به Prefixing دارد.
- برای Angular طراحی نشدهاست.
جهت رفع این مشکلات و محدودیتها، تیم Angular کتابخانهای را به نام Angular Flex Layout مخصوص نگارشهای جدید Angular طراحی کردهاست. این کتابخانه مستقل از Angular Material است اما عموما به همراه آن استفاده میشود.
مزایای کار با کتابخانهی Angular flex layout
- پیاده سازی TypeScript ایی دارد. در اصل یک سری directives مخصوص Angular است که با TypeScript نوشته شدهاست.
- به صورت پویا و inline تمام CSSهای مورد نیاز را تولید و تزریق میکند.
- به همراه یک API استاتیک است و همچنین یک API واکنشگرا
- با Angular CLI نیز یکپارچه شدهاست.
نصب و تنظیم کتابخانهی Angular Flex layout
برای نصب این کتابخانه، در ریشهی پروژه دستور زیر را صادر کنید:
npm install @angular/flex-layout --save
import { FlexLayoutModule } from "@angular/flex-layout"; @NgModule({ imports: [ FlexLayoutModule ], exports: [ FlexLayoutModule ] }) export class SharedModule { }
کار با API استاتیک Angular Flex layout
API استاتیک Angular Flex layout شامل این مزایا و مشخصات است:
- به صورت یکسری دایرکتیو Angular طراحی شدهاست که به HTML قالب کامپوننتها اضافه میشود.
- از data binding پشتیبانی میکند.
- CSS نهایی را به صورت پویا و inline تولید و به صفحه تزریق میکند. Inline CSS تزریق شده به ویژگیهای styles هر المان تزریق میشوند و موارد مشابه را در صورت وجود بازنویسی میکنند.
- از تشخیص تغییرات پشتیبانی میکند.
- به همراه ویژگیهای fxHide و fxShow است.
- کارآیی مطلوبی دارد.
در اینجا برای تعریف container اصلی از دایرکتیوهای زیر استفاده میشود:
- fxLayout جهتهای flex را مشخص میکند.
<div fxLayout="row" fxLayout.xs="column"></div>
- fxLayoutWrap مشخص میکند که آیا المانها باید به سطر و یا ستون بعدی منتقل شوند یا خیر؟
<div fxLayoutWrap></div>
<div fxLayoutGap="10px"></div>
<div fxLayoutAlign="start stretch"></div>
چند مثال:
و یا حالت راست به چپ آن به صورت زیر است:
و برای تعریف آیتمهای قرار گرفتهی درون containers میتوان از دایرکتیوهای زیر استفاده کرد:
- fxflex برای تعیین اندازه و flex المانها
<div fxFlex="1 2 calc(15em + 20px)"></div>
fxFlex="grow shrink basis"
fxFlex="basis"
- shrink به این معنا است که اگر به اندازهی کافی فضا وجود نداشت، این المان نسبت به سایر المانهای دیگر تا چه اندازهای میتواند کوچک شود.
- basis به معنای اندازهی پیشفرض المان است.
در اینجا اندازهها برحسب پیکسل، درصد و یا calcs, em, cw, vh میتوانند تعیین شوند. همچنین یک سری نام مستعار مانند grow, initial, auto, none, nogrow, noshrink هم قابل استفاده هستند.
- fxflexorder برای تعیین ترتیب قرارگیری یک المان
<div fxFlexOrder="2"></div>
<div fxFlexOffset="20px"></div>
<div fxFlexAlign="center"></div>
<div fxFlexFill></div>
چند مثال:
در اینجا سه نمایشی را که در ذیل تعریف divها مشاهده میکنید بر اساس تغییر اندازهی صفحه حاصل شدهاند. چون آیتم دوم دارای مقدار grow مساوی 5 است، به همین جهت با تغییر اندازهی صفحه و دسترسی به مقدار فضای بیشتر، بزرگتر شدهاست.
یک مثال کامل
اگر علاقمند باشید تا توانمندیهای angular flex layout را در قالب یک مثال کامل مشاهده کنید، به آدرس زیر مراجعه نمائید:
https://tburleson-layouts-demos.firebaseapp.com/#/docs
در این مثال با تغییر گزینههای مختلف، کد معادل angular flex layout آن نیز تولید میشود.
همچنین wiki خود پروژه نیز به همراه مثالهای بیشتری است:
https://github.com/angular/flex-layout/wiki
کار با API واکنشگرای Angular Flex layout
در طراحی واکنشگرا، container و عناصر داخل آن بر اساس تغییرات اندازهی صفحه و یا اندازهی وسیلهی نمایشی، تغییر اندازه و همچنین موقعیت میدهند و این تغییرات بر اساس انطباق با viewport وسیلهی نمایشی صورت میگیرند. به همین جهت برای طراحی واکنشگرا نیاز به Flex CSS و همچنین Media Query است. نوشتن Medial Query و ترکیب آن با Flex CSS کار مشکلی است. به همین جهت Angular Flex layout به همراه یک API واکنشگرا نیز هست که در پشت صحنه Flex CSS را بر اساس طراحی متریال و Medial Queries مورد استفاده قرار میدهد.
اگر علاقمند هستید تا اندازههای واکنشگرای استاندارد متریال را ملاحظه کنید، میتوانید به آدرس زیر مراجعه نمائید (قسمت Breakpoint system آن):
https://material.io/design/layout/responsive-layout-grid.html#breakpoints
برای مثال هر اندازهای کمتر از 600px در گروه extra small قرار میگیرد (با مخفف xs). از 600px تا 1024px در بازهی small (با مخفف sm)، از 1024px تا 1440px در بازهی medium (با مخفف md) و از 1440px تا 1920px در بازهی large (با مخفف lg) و بیشتر از آن در بازهی xlrage قرار میگیرند (با مخفف xl). این اعداد و بازهها، پایهی طراحی API واکنشگرای Angular Flex layout هستند. به همین جهت نام این بازهها در این API به صورت مخفف xs, sm, md, lg, xl درنظر گرفته شدهاند و مورد استفاده قرار میگیرند. همچنین اگر اندازههای مدنظر از این بازهها کمتر باشند، میتوان از lt-sm, lt-md, lt-lg, lt-xl نیز استفاده کرد. در اینجا lt به معنای less than است و یا اگر بازههای مورد نیاز بیش از این اندازهها باشند میتوان با gt-xs, gt-sm, gt-md, gt-lg کار کرد. در اینجا gt به معنای greater than است.
به این مخففها «media query alias» هم گفته میشود و اکنون که لیست آنها مشخص است، تنها کافی است آنها را به API استاتیکی که پیشتر بررسی کردیم، اضافه کنیم. برای مثال:
fxLayout.sm = "..." fxLayoutAlign.md = "..." fxHide.gt-sm = "..."
معادل طراحی آن با API استاتیک Angular Flex Layout به صورت زیر است:
اکنون که این طرحبندی دسکتاپ را داریم، چگونه باید آنرا تبدیل به طرحبندی موبایل، مانند شکل زیر کنیم؟
برای اینکار ابتدا fxLayout.xs را به سطر میانی اضافه میکنیم تا هرگاه به این اندازه رسیدیم، بجای ردیف، تبدیل به ستون شود. سپس توسط fxFlexOrder.xs، در اندازهی xs، محل قرارگیری المانهای این ستون را هم مشخص میکنیم:
همانطور که ملاحظه میکنید کار کردن با این API بسیار سادهاست و نیازی به کارکردن مستقیم با Media Queries و یا برنامه نویسی مستقیم ندارد و تمام آن در قالب HTML یک کامپوننت قابل پیاده سازی است.
یک نکته: مثال کاملی که پیشتر در این بحث مطرح شد، به همراه مثال واکنشگرا نیز هست که برای مشاهدهی اثر آنها بهتر است اندازهی مرورگر را کوچک و بزرگ کنید.
مخفی کردن و یا نمایش قسمتی از صفحه بر اساس اندازهی آن
علاوه بر media query alias هایی که عنوان شد، امکان نمایش و یا مخفی سازی قسمتهای مختلف صفحه بر اساس اندازهی صفحهی نمایشی نیز هست:
<div fxShow fxHide.xs="false" fxHide.lg="true"></div>
تغییر اندازهی قسمتی از صفحه بر اساس اندازهی آن
در مثال زیر اگر اندازهی صفحه gt-sm باشد (بیشتر از small)، اندازهی این div به 100 درصد بجای 50 درصد حالتهای دیگر، تنظیم میشود:
<div fxFlex="50%" fxFlex.gt-sm="100%"></div>
حالتهای ویژهی طراحی واکنشگرا در Angular Flex Layout
در API واکنشگرای آن حالتهای ویژهی fxshow, fxhide, ngclass و ngstyle نیز درنظر گرفته شدهاند که امکان فعالسازی آنها در اندازههای مختلف صفحه مسیر است:
<div fxShow [fxShow.xs]="isVisibleOnMobile()"></div> <div fxHide [fxHide.gt-sm]="isVisibleOnDesktop()"></div> <div [ngClass.sm]="{'fxClass-sm': hasStyle}" ></div> <div [ngStyle.xs]="{color: 'blue'}"></div>
امکان کار با API واکنشگرا از طریق برنامه نویسی
برای این منظور میتوان از سرویس ObservableMedia مانند مثال زیر استفاده کرد:
در اینجا به فعالسازی یک بازهی خاص گوش فرا خواهیم داد. برای مثال اگر اندازهی صفحه xs بود، سبب بارگذاری محتوای خاص مرتبط با موبایل خواهیم شد.
برای مطالعهی بیشتر
Install-Package Breeze.WebApi2.EF6
var instance = breeze.config.initializeAdapterInstance("ajax", "angular"); instance.setHttp($http);
Install-Package Breeze.Angular
var manager = new breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) });
var dataService = new breeze.DataService({ serviceName: "/breeze/"+ "Automobile", hasServerMetadata: false, namingConvention: breeze.NamingConvention.camelCase }); var metadataStore = new breeze.MetadataStore({});
var myMetadataStore = new breeze.MetadataStore(); myMetadataStore.addEntityType({...});
var customer = function () { this.City = ""; }; myMetadataStore.registerEntityTypeCtor("Customer", customer);
[BreezeController] public class AutomobileController : ApiController { readonly EFContextProvider<ApplicationDbContext> _contextProvider = new EFContextProvider<ApplicationDbContext>(); [HttpGet] public string Metadata() { return _contextProvider.Metadata(); } [HttpGet] public IQueryable<Customer> Customers() { return _contextProvider.Context.Customers; } [System.Web.Http.HttpPost] public SaveResult SaveChanges(JObject saveBundle) { _contextProvider.BeforeSaveEntitiesDelegate = BeforeSaveEntities; _contextProvider.AfterSaveEntitiesDelegate = afterSaveEntities; return _contextProvider.SaveChanges(saveBundle); } protected Dictionary<Type, List<EntityInfo>> BeforeSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap) { } private void afterSaveEntities(Dictionary<Type, List<EntityInfo>> saveMap, List<KeyMapping> keyMappings) { } }
manger.saveChanges().then(function success() { }, function failer(e) { });
manger.rejectChanges()
کوئری:بعد از تعریف Entity Manger میتوانیم کوئری خود را اجرا نماییم. کوئری ما شامل گرفتن اطلاعات از جدول Customer، با مرتب سازی بر روی فیلد آیدی میباشد و با اجرا کردن کوئری میتوانیم موفقیت یا عدم موفقیت آنرا بررسی نماییم.
var query = breeze.EntityQuery .from("Customer") .orderBy("Id"); var result= manager.executeQuery(query); result.then(querySucceeded) .fail(queryFailed); query = query.where("Id", "==", 1)
var predicate = new breeze.Predicate("Id", "==", false); query = query.where(predicate) var p1 = new breeze.Predicate("IsArchived", "==", false); var p2 = breeze.Predicate("IsDone", "==", false); var predicate = p1.and(p2); query = query.where(predicate).orderBy("Id")
?$filter=IsArchived eq false&IsDone eq false &$orderby=Id
اعتبارسنجی :اعتبارسنجی در breeze، هم در سمت کلاینت و هم در سمت سرور امکان پذیر میباشد که در مثالی، در قسمت بعدی، validator سفارشی خودمان را خواهیم ساخت و به entity مورد نظر اعمال خواهیم کرد.
breeze.Validator.required({ allowEmptyStrings: true });
ردیابی تغییرات: هر آیتم Entity دارای EntityAspect است که وضعیت آنرا مشخص میکند و میتواند یکی از وضعیتهای Added،Modified،Deleted،Detached،Unchanged باشد. با مشخص کردن حالت هر آیتم، با فراخوانی SaveChanges تغییرات بر روی دیتابیس اعمال میگردد.
manager.createEntity('Customer', jsonValue);
manager.createEntity("Customer", jsonValue, breeze.EntityState.Modified, breeze.MergeStrategy.OverwriteChanges)
manager.createEntity("Customer", item, breeze.EntityState.Deleted)
برای اشنایی بیشتر با امکانات Breeze، قصد داریم یک سایت ایجاد آگهی را راه اندازی کنیم. پیش نیازهای ضروری این بخش typescript ،angularjs ،requirejs هستند. قصد داریم سایتی را برای آگهیهای خرید و فروش خودرو، مشابه با سایت باما ایجاد نماییم:
امکانات این سایت:
Install-Package angularjs Install-Package angularjs.TypeScript.DefinitelyTyped Install-Package bootstrap Install-Package bootstrap.TypeScript.DefinitelyTyped Install-Package jQuery Install-Package jquery.TypeScript.DefinitelyTyped Install-Package RequireJS Install-Package requirejs.TypeScript.DefinitelyTyped bower install angularAMD
مدلهای برنامه:
public class BaseEntity { public int Id { get; set; } public bool Status { get; set; } public DateTime CreatedDateTime { get; set; } }
public class Ad : BaseEntity { public string Title { get; set; } public float Price { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string UserId { get; set; } public DateTime ModifieDateTime { get; set; } public string Description { get; set; } public virtual ICollection<Comment> Comments { get; set; } public virtual IdentityUser User { get; set; } public virtual ICollection<AdLabel> Labels { get; set; } public virtual ICollection<AdMedia> Medias { get; set; } }
public class Label { public int Id { get; set; } public string Title { get; set; } public int? ParentId { get; set; } public virtual Label Parent { get; set; } public virtual ICollection<Label> Items { get; set; } }
public class Media { public int Id { get; set; } public string Name { get; set; } public string MimeType { get; set; } }
public class AdLabel { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Label Label { get; set; } [Index("IX_AdLabel", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdLabel", 2, IsUnique = true)] public int LabelId { get; set; } public string Value { get; set; } }
public class AdMedia { public int Id { get; set; } public virtual Ad Ad { get; set; } public virtual Media Media { get; set; } [Index("IX_AdMedia", 1, IsUnique = true)] public int AdId { get; set; } [Index("IX_AdMedia", 2, IsUnique = true)] public int MediaId { get; set; } }
public class Comment : BaseEntity { public string Body { get; set; } public double Rating { get; set; } public int? RatingNumber { get; set; } public string EntityName { get; set; } public string UserId { get; set; } public int? ParentId { get; set; } public int? AdId { get; set; } public virtual Comment Parent { get; set; } public virtual Ad Ad { get; set; } public virtual ICollection<Comment> Items { get; set; } public virtual IdentityUser User { get; set; } }
public class Customer:BaseEntity { public string UserId { get; set; } public virtual string DisplayName { get; set; } public virtual string BirthDay { get; set; } public string City { get; set; } public string Address { get; set; } public int? MediaId { get; set; } public bool? NewsLetterSubscription { get; set; } public string PhoneNumber { get; set; } public virtual IdentityUser User { get; set; } public virtual Media Media { get; set; } }
public class Rating { public int Id { get; set; } public string UserId { get; set; } public Double Rate { get; set; } public string EntityName { get; set; } public int DestinationId { get; set; } }
اضافه کردن مدلهای برنامه به ApplicationDbContext
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public DbSet<Ad> Ads { get; set; } public DbSet<AdLabel> AdLabels { get; set; } public DbSet<AdMedia> AdMedias { get; set; } public DbSet<Comment> Comments { get; set; } public DbSet<Label> Labels { get; set; } public DbSet<Media> Medias { get; set; } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
لود کردن فایل main.js در فایل layout.cshtml ترجیحا در انتهای body
<script src="~/Scripts/require.js" data-main="/app/main"></script>
ایجاد فایل main.ts
requirejs.config({ paths: { "app": "app", "angularAmd":"/Scripts/angularAmd", "angular": "/Scripts/angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "app": { deps: ["bootstrap","angularRoute"] } } }); require(["app"]);
ایجاد فایل app.ts: کارهایی که در فایل app انجام دادهایم:
<body ng-controller="SecurityCtrl"> ... </body>
"use strict"; module AdApps { class SecurityCtrl { private $scope: Interfaces.IAdvertismentScope; constructor($scope: Interfaces.IAdvertismentScope) { // security check this.$scope = $scope; } } define(["angularAmd", "angular"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', $routeProvider => { $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home .js" })) .otherwise({ redirectTo: '/' }); } ]); app.controller('SecurityCtrl', ['$scope', SecurityCtrl]); return angularAmd.bootstrap(app); })}
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت چهارم - تکمیل مستندات نوعهای خروجی API
در برنامههای ASP.NET Core، اطلاعات OpenAPI بر اساس سرویس توکاری به نام ApiExplorer تولید میشود که کار آن فراهم آوردن متادیتای مرتبط با یک برنامهی وب است. برای مثال توسط این سرویس میتوان به لیست کنترلرها، متدها و پارامترهای آنها دسترسی یافت. Swashbuckle.AspNetCore به کمک ApiExplorer کار تولید OpenAPI Specification را انجام میدهد. برای فعالسازی این سرویس نیازی نیست کار خاصی انجام شود و زمانیکه ()services.AddMvc را فراخوانی میکنیم، ثبت و معرفی این سرویس نیز جزئی از آن است.
اهمیت تولید Response Types صحیح
در قسمتهای قبل مشاهده کردیم که اگر متدی برای مثال در قسمتی از آن return NotFound یا 404 را داشته باشد، این نوع از خروجی، در OpenAPI Specification تولیدی لحاظ نمیشود و ناقص است و یا حتی ممکن است Response Type پیشفرض تولیدی که 200 است، ارتباطی به هیچکدام از نوعهای خروجی یک اکشن متد نداشته باشد و نیاز به اصلاح آن است. این مورد برای تکمیل مستندات یک API ضروری است و استفاده کنندگان از یک API باید بدانند چون نوع خروجیهایی را ممکن است در شرایط مختلف، دریافت کنند.
روش تغییر و اصلاح Response Type پیشفرض OpenAPI Specification
اکشن متد GetBook کنترلر کتابها، دارای دو نوع return Ok و return NotFound است؛ اما OpenAPI Specification تولیدی پیشفرض، تنها حالت return Ok یا 200 را مستند میکند. برای تکمیل مستندات این اکشن متد، میتوان به صورت زیر عمل کرد:
/// <summary> /// Get a book by id for a specific author /// </summary> /// <param name="authorId">The id of the book author</param> /// <param name="bookId">The id of the book</param> /// <returns>An ActionResult of type Book</returns> /// <response code="200">Returns the requested book</response> [HttpGet("{bookId}")] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
در اینجا StatusCodes.Status400BadRequest را نیز مشاهده میکنید. هرچند حالت return BadRequest در کدهای این اکشن متد وجود خارجی ندارد، اما در صورت بروز مشکلی در فراخوانی و یا پردازش آن، به صورت خودکار توسط فریمورک بازگشت داده میشود. بنابراین مستندسازی آن نیز ضروری است.
برای آزمایش آن، برنامه را اجرا کنید. در قسمت مستندات متد فوق، اکنون سه حالت 404، 400 و 200 قابل مشاهده هستند. برای نمونه بر روی دکمهی try it out آن کلیک کرده و زمانیکه authorId و bookId را درخواست میکند، دو Guid اتفاقی و کاملا بیربط را وارد کنید. همچنین Controls Accept header را نیز بر روی application/json قرار دهید. سپس بر روی دکمهی execute در ذیل آن کلیک نمائید. یک چنین خروجی 404 کاملی را مشاهده خواهید کرد:
در این تصویر، response body بر اساس rfc 7807 تولید میشود و استاندارد گزارش مشکلات یک API است. این مورد اکنون به صورت یک اسکیمای جدید در انتهای مستندات تولیدی نیز قابل مشاهدهاست:
بهبود مستندات تشخیص نوعهای مدلهای خروجی اکشن متدها
مورد دیگری که در اینجا جالب توجه است، تشخیص نوع خروجی، در حالت return Ok است:
در اینجا اگر بر روی لینک Schema، بجای Example value پیشفرض کلیک کنیم، تصویر فوق حاصل میشود. تشخیص این نوع، به علت استفادهی از ActionResult از نوع Book، به صورت زیر است که در ASP.NET Core 2.1 برای همین منظور (تکمیل مستندات Swagger) معرفی شدهاست:
public async Task<ActionResult<Book>> GetBook(Guid authorId, Guid bookId)
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Book))]
یک نکته: در این تصویر، در قسمت توضیحات حالت 200، عبارت "Returns the requested book" مشاهده میشود. اما در حالتهای دیگر response types تعریف شده، عبارات پیشفرض bad request و یا not found نمایش داده شدهاند. نحوهی بازنویسی این پیشفرضها، با تکمیل مستندات XMLای اکشن متد و ذکر response code دلخواه، به صورت زیر است:
/// <response code="200">Returns the requested book</response>
استفاده از API Analyzers برای بهبود OpenAPI Specification تولیدی
اکنون این سؤال مطرح میشود که پس از این تغییرات، هنوز چه مواردی در OpenAPI Specification تولیدی ما وجود خارجی ندارند و بهتر است اضافه شوند. برای پاسخ به این سؤال، از زمان ارائهی ASP.NET Core 2.2، بستهی جدید Microsoft.AspNetCore.Mvc.Api.Analyzers نیز ارائه شدهاست که کار آن دقیقا بررسی همین نقایص و کمبودها است. بنابراین ابتدا آنرا به فایل OpenAPISwaggerDoc.Web.csproj اضافه کرده و سپس دستور dotnet restore را صادر میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc.Api.Analyzers" Version="2.2.0" /> </ItemGroup>
Controllers\BooksController.cs(40,17): warning API1000: Action method returns undeclared status code '404'. Controllers\BooksController.cs(89,13): warning API1000: Action method returns undeclared status code '201'.
/// <summary> /// Get the books for a specific author /// </summary> /// <param name="authorId">The id of the book author</param> /// <returns>An ActionResult of type IEnumerable of Book</returns> [HttpGet()] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesDefaultResponseType] public async Task<ActionResult<IEnumerable<Book>>> GetBooks(Guid authorId)
کار آن مدیریت تمام حالتهای دیگری است که هنوز توسط ProducesResponseTypeها تعریف یا پیشبینی نشدهاند. هرچند وجود آن میتواند در یک چنین مواردی مفید باشد، اما همواره تعریف صریح نوعهای خروجی نسبت به استفادهی از یک حالت پیشفرض برای تمام آنها، ترجیح داده میشود.
ساده سازی کدهای تکراری تعریف ProducesResponseTypeها
مواردی مانند StatusCodes.Status400BadRequest و یا 406 را در حالت عدم قبول درخواست (مثلا با انتخاب یک accept header اشتباه) و یا 500 را در صورت وجود استثنائی در سمت سرور، باید به تمام اکشن متدها نیز اضافه کرد؛ چون میتوانند تحت شرایطی، نوعهای خروجی معتبری باشند. برای خلاصه سازی این عملیات، یا میتوان این ویژگیها را بجای قراردادن آنها در بالای تعریف امضای اکشن متدها، به بالای تعریف کلاس کنترلر انتقال داد. با اینکار ویژگی که به یک کنترلر اعمال شده باشد به تمام اکشن متدهای آن نیز اعمال خواهد شد و یا حتی برای عدم تعریف این ویژگیهای تکراری به ازای هر کنترلر موجود، میتوان آنها را به صورت سراسری تعریف کرد:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(setupAction => { setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status400BadRequest)); setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status406NotAcceptable)); setupAction.Filters.Add( new ProducesResponseTypeAttribute(StatusCodes.Status500InternalServerError)); setupAction.Filters.Add( new ProducesDefaultResponseTypeAttribute()); setupAction.ReturnHttpNotAcceptable = true; // ...
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-04.zip
در قسمت بعد، روشهای دیگری را برای تکمیل مستندات خروجی API بررسی میکنیم.
مقدمه ای بر AutoMapper
install-package automapper
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection
using AutoMapper;
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAutoMapper(); }
public class UserMappingProfile:Profile { public UserMappingProfile() { CreateMap<User, UserPost>(); CreateMap<UserPost,User >() .ForMember(dest=>dest.Username,src=>src.Ignore()) .ForMember(dest=>dest.Email,src=>src.Ignore()); } }
private readonly IMapper _mapper; public UserController(IMapper mapper) { _mapper = mapper; } public async Task<UserPost> FindUser(string username) { var users = await _userServices.GetUser(username); var user = _mapper.Map<User,UserPost>(users); return user; }
سشن چیست؟
شیء سشن، مجموعهای از اشیاء serialized مرتبط با جلسهی کاری جاری یک کاربر است. این اشیاء عموما در حافظهی محلی سرور ذخیره میشوند؛ اما امکان ذخیره سازی توزیع شدهی آنها در بانکهای اطلاعاتی نیز پیش بینی شدهاست.
عموما استفادهی از اشیاء سشن توصیه نمیشوند. از این جهت که این نوع اشیاء بسیار شبیه هستند به متغیرهای سراسری و وجود این نوع متغیرها اساسا ضعف طراحی شیءگرا به حساب میآیند. اما با توجه به ماهیت stateless بودن برنامههای وب، به این معنا که با پایان رندر یک صفحه، تمام اشیاء مرتبط با آنها نیز در سمت سرور تخریب میشوند، نیاز است برای یک سری از دادههای عمومی کاربر، راه حلی را پیدا کرد تا بتوان از اطلاعات آنها استفادهی مجدد کرد. برای مثال نگهداری رشتهی اتصالی بانک اطلاعاتی که کاربر در حین لاگین به سیستم آنرا انتخاب کردهاست (اگر برنامه به ازای هر سال از یک بانک اطلاعاتی مجزا استفاده میکند) و یا زمانیکه کاربری captcha را پر میکند و مقدار آنرا به سمت سرور ارسال میکند، نیاز است مقدار ارسالی او را با مقدار ابتدایی captcha مقایسه کرد. یک چنین اطلاعاتی نباید با پایان رندر صفحه تخریب شوند و نیاز است تا زمانیکه جلسهی کاری کاربر به پایان نرسیدهاست، در دسترس باشند. به همین جهت است که مفهومی را به نام «اشیاء سشن» طراحی کردهاند.
درکل خارج از این موارد بهتر است از سشن استفاده نکنید و در جای جای برنامهی خود ردپای آنرا باقی نگذارید و به خاطر داشته باشید:
متغیر سشن = متغیر سراسری = ضعف طراحی شیءگرا
توصیهی به استفادهی از روشهای سبک وزنتر
سشنها تنها روش به اشتراک گذاری اطلاعات نیستند. اگر میخواهید اطلاعاتی را در بین میان افزارهای برنامه در طی یک درخواست به اشتراک بگذارید، شاید سشن هم یک راه حل باشد؛ اما راه حلی سنگین وزن. راه حل بهتر برای این موارد، استفادهی از HttpContext.Items است. HttpContext.Items نیز همانند سشن، یک key/value store است؛ اما طول عمر آن محدود است به طول عمر درخواست جاری و در تمام میان افزارهای برنامه در دسترس است.
برای مثال در یک میان افزار آنرا تنظیم میکنید:
app.Use(async (context, next) => { context.Items["isVerified"] = true; await next.Invoke(); });
app.Run(async (context) => { await context.Response.WriteAsync("Verified request? " + context.Items["isVerified"]); });
فعال سازی سشنها در ASP.NET Core
ASP.NET Core یک choose-what-you-need framework است. به این معنا که تا زمانیکه قابلیتی را به صورت صریح فعال سازی نکرده باشید، در دسترس نخواهد بود. همین مساله در نهایت به کاهش مصرف منابع این نوع برنامهها و همچنین طراحی ماژولار سیستم ختم میشوند. برای مثال در نگارشهای قبلی ASP.NET (تمام نگارشها)، سشنها به صورت پیش فرض فعال هستند، مگر آنکه HTTP Module آنرا در فایل web.config حذف کنید؛ اما در اینجا برعکس است.
اگر تنها در موارد خاصی که ذکر شد، نیاز به استفادهی از متغیرهای سشن را داشتید، روش فعال سازی آن به صورت ذیل است:
الف) نصب بستهی نیوگت Microsoft.AspNetCore.Session
برای این منظور وابستگی ذیل را به فایل project.json اضافه کنید:
{ "dependencies": { //same as before "Microsoft.AspNetCore.Session": "1.0.0" } }
برای این کار به کلاس آغازین برنامه مراجعه کرده و ابتدا سرویس سشنها را فعال کنید:
public void ConfigureServices(IServiceCollection services) { services.AddSession();
public void Configure(IApplicationBuilder app) { app.UseSession();
app.UseSession(options: new SessionOptions { IdleTimeout = TimeSpan.FromMinutes(30), CookieName = ".MyApplication" });
روش استفادهی از سشنها
در مثال ذیل نحوهی ذخیره سازی اطلاعات را در شیء سشن جلسهی جاری یک کاربر، ملاحظه میکنید:
public ActionResult TestSession() { this.HttpContext.Session.Set("key-1", BitConverter.GetBytes(DateTime.Now.Ticks)); this.HttpContext.Session.SetInt32("key-2", 1); this.HttpContext.Session.SetString("key-3", "DNT"); return Content("OK!"); }
public IActionResult Index() { byte[] key1 = this.HttpContext.Session.Get("key-1"); long key1Value = BitConverter.ToInt64(key1, 0); int? key2Value = this.HttpContext.Session.GetInt32("key-2"); string key3Value = this.HttpContext.Session.GetString("key-3"); return Content("OK!"); }
حالت Set آن آرایهای از بایتها را دریافت میکند و میتوان برای حالت serialization اشیاء، مفید باشد.
دقیقا معادل همین سه متد، متدهای Get، GetInt32 و GetString برای بازیابی مقادیر سشن طراحی شدهاند و باید دقت داشت که خروجیهای اینها میتوانند نال نیز باشند. به همین جهت خروجی GetInt32 آن نال پذیر است.
توسعهی متدهای پیش فرض کار با سشنها
سه متد یاد شدهی کار با سشنها در ASP.NET Core هرچند ضروری هستند، اما کافی نیستند. برای توسعهی آنها میتوان متدهای الحاقی را تدارک دید که نمونهای از آنها را ذیل مشاهده میکنید:
using System; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; namespace Core1RtmEmptyTest.StartupCustomizations { public static class SessionExts { public static void SetDateTime(this ISession collection, string key, DateTime value) { collection.Set(key, BitConverter.GetBytes(value.Ticks)); } public static DateTime? GetDateTime(this ISession collection, string key) { var data = collection.Get(key); if (data == null) { return null; } var dateInt = BitConverter.ToInt64(data, 0); return new DateTime(dateInt); } public static void SetObject(this ISession session, string key, object value) { var stringValue = JsonConvert.SerializeObject(value); session.SetString(key, stringValue); } public static T GetObject<T>(this ISession session, string key) { var stringValue = session.GetString(key); return JsonConvert.DeserializeObject<T>(stringValue); } } }
و یا جهت کار با اشیاء پیچیدهتر میتوان از کتابخانهی JSON.NET استفاده کرد. به عبارتی در این نگارش از ASP.NET، کار سریالایز و دیسریالایز اشیاء، به برنامه نویس واگذار شدهاست و اینکه در پشت صحنه از چه کتابخانهای میخواهید استفاده کنید، در اختیار خودتان است.
البته باید دقت داشت که در اینجا وابستگی JSON.NET به صورت خودکار در دسترس است. از این جهت که بسیاری از وابستگیهای ASP.NET Core مانند مورد ذیل، به JSON.NET وابستهاند و نصب آنها به معنای نصب خودکار JSON.NET نیز هست:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Json": "1.0.0" } }
یک مطلب تکمیلی
در اینجا نیز امکان ذخیره سازی سشنها در بانک اطلاعاتی بجای حافظهی فرار سرور درنظر گرفته شدهاست و برای این حالت، بانکهای اطلاعاتی NoSQL ویژهای به نام key/value stores مانند بانک اطلاعاتی فوق سریع Redis پیشنهاد میشود؛ هرچند امکان کار با SQL Server نیز در اینجا وجود دارد، اما برای کش سرورهای مبتنی بر key/value ها، بانک اطلاعاتی Redis، انتخاب اول است.
Managing Application State
تصمیم گرفتم در طی چندین پست در حد توانم به آموزش jQuery بپردازم. (مطالب نوشته شده برداشت ازادی از کتاب jQuery in action است)
جی کوئری (jQuery) چیست؟
نکته: برای استفاده از جی کوئری باید HTML و CSS و جاوا اسکریپت آشنایی داشته باشید.
چگونه از جی کوئری استفاده کنیم؟
برای استفاده از جی کوئری باید ابتدا فایل آن را از سایت آن دانلود کرده و در پروژه خود استفاده نمایید. البته روشهای دیگری برای استفاده از این فایل وجود دارد که در آینده بیشتر با آن آشنا خواهیم شد. برای استفاده از این فایل در پروژه باید به شکل زیر آن را به صفحه HTML خود معرفی کنیم.
<html> <head> <script type="text/javascript" src="jquery-1.9.1.min.js"></script> </head> <body> </body> </html>
کوتاه کردن کد: هر زمان شما خواسته باشید کارکرد یک صفحه وب را پویاتر کنید، در اکثر مواقع به ناچار این کار از طریق عناصری بروی صفحه انجام داده اید که با توجه به انتخاب شدن آنها، صفحه کارکردی خاص خواهد داشت. مثلا در جاوا اسکریپت اگر بخواهیم عنصری را که در یک radioGroup انتخاب شده است را برگردانیم باید کدهای زیر را بنویسیم:
var checkedValue; var elements = document.getElementByTagName ('input'); for (var n = 0; n < elements.length; n++) { if (elements[n].type == 'radio' && elements[n].name == 'myRadioGroup' && elements[n].checked) { checkedValue = elements[n].value; } }
var checkedValue = $ ('[name="myRadioGroup"]:checked').val();
قدرت اصلی جی کوئری برگفته از انتخابکنندهها (Selector) هاست، انتخابکننده ، یک عبارت است که دسترسی به عنصری خاص بر روی صفحه را موجب میشود؛ انتخابکننده این امکان را فراهم میسازد تا به سادگی عنصر مورد نظر را مشخص و به آن دسترسی پیدا کنیم که در مثال فوق، عنصر مورد نظر ما گزینه انتخاب شده از myRadioGroup بود.
Unobtrusive JavaScript: اگر پیش از پیدایش CSS در کار ایجاد صفحههای اینترنتی بودهاید حتما مشکلات و مشقات آن دوران را به خاطر میآورید. در آن زمان برای فرمتدهی به اجزای مختلف صفحه ، به ناچار علائم فرمتدهی را به همراه دستورات خود اجزا، در صفحههای HTML استفاده میکردیم. اکنون بسیار بعید به نظر میرسد کسی ترجیح دهد فرمتدهی اجزا را به همراه دستورهای HTML آن انجام دهد. اگر چه هنوز دستوری مانند زیر بسیار عادی به نظر میآید:
<button type="button" onclick="document.getElementById('xyz').style.color='red';"> Click Me </button>
مجموعه عناصر در جی کوئری:
زمانی که CSS به عنوان یک تکنولوژی به منظور جداسازی طراحی از ساختار به دنیای صفحههای اینترنتی معرفی شد، میبایست راهی برای اشاره به اجزای صفحات از طرف فایل CSS نیز معرفی میشد. این امر از طریق انتخابکنندهها (Selector) صورت پذیرفت.
برای مثال انتخابکننده زیر، به تمام عناصر <a> اشاره دارد که در یک عنصر <p> قرار گرفتهاند:
p a
برای انتخاب مجموعهای از عناصر از یکی از دو Syntax زیر استفاده میکنیم.
$(Selector) یا jQuery(Selector)
مثال زیر نمونهای دیگر است که در آن مجموعهای از تمام لینکهایی که درون تگ <p> قرار دارند را انتخاب میکند:
$("p a")
در اصطلاح برنامه نویسی به چنین توابعی که گروهی از عناصر را جمع میکنند، Wrapper میگویند زیرا تمام عناصر مطلوب را تحت یک شی بستهبندی میکند. در جیکوئری به آنها Wrapped Set یا jQuery Wrapper میگویند و به متدهایی که قابل اعمال بروی اینها به نام jQuery Wrapper Methodes شناخته میشوند.
در مثال زیر میخواهیم تمام عناصر <div> در صورتی که دارای کلاس notLongForThisWorldباشند را مخفی (با فید شدن) کنیم.
$("div.notLongForThisWorld").fadeOut();
فرض کنید در مثال بالا بخواهیم پس از مخفی کردن هر <div> بخواهیم یک کلاس به نام removedبه آن بیافزاییم. به این منظور میتوان کدی مانند زیر نوشت:
$("div.notLongForThisWorld").fadeOut().addClass("removed");
چند نمونه انتخاب کننده:
نتیجه | انتخاب کننده | |
تمام <p>های زوج را انتخاب میکند | $('p:even') | |
سطر اول هر جدول را انتخاب میکند | $("tr:nth-child(1)"); | |
<div>هایی که مستقیما در <body> تعریف شده باشند را انتخاب میکند. | $("body > div"); | |
لینک هایی که به یک فایل pdf اشاره دارند را انتخاب میکند. | $("a[href$=pdf]"); | |
تمام <div> هایی که مستقیما در <body> معرفی شده اند و دارای لینک میباشند را انتخاب میکند. | $("body > div:has(a)") | |
ادامه مطالب در پستهای بعدی تشریح خواهد شد.
جهت مطالعه بیشتر میتوانید از این منابع ^ و ^ و ^ و ^ و ^ استفاده کنید.
موفق و موید باشید
ASP.NET MVC #11
بررسی نکات تکمیلی Model binder در ASP.NET MVC
یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:
using System;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
از این مدل چند مقصود ذیل دنبال میشوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. میتواند به صورت خودکار ورودیهای کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!
در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرسهای مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:
using System;
using System.Collections.Generic;
namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}
در اینجا فعلا هدف آشنایی با زیر ساختهای ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده میکنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شدهاند. تهیه یک ToList یا Insert و Update با این فریم ورکها خارج از بحث جاری هستند.
سورس کامل کنترلر User به شرح زیر است:
using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;
namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}
[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}
[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}
[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}
[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}
[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}
توضیحات:
ایجاد خودکار فرمهای ورود اطلاعات
در قسمت قبل برای توضیح دادن نحوه ایجاد فرمها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آنها به اکشن متدهای کنترلرها، فرمهای مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها میتوان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرمهای تولیدی را سفارشی نمود. این سریعترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و میخواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینهی create a strongly typed view را انتخاب کرده و سپس از لیست مدلها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، میتوان یک کاربر را یافته و حذف کرد.
سفارشی سازی Viewهای خودکار تولیدی
با کمک امکانات Scaffolding نامبرده شده، حجم قابل توجهی کد را در اندک زمانی میتوان تولید کرد. بدیهی است حتما نیاز به سفارشی سازی کدهای تولیدی وجود خواهد داشت. مثلا شاید نیازی نباشد فیلد پسود کاربر، در حین نمایش لیست کاربران، نمایش داده شود. میشود کلا این ستون را حذف کرد و از این نوع مسایل.
یک مورد دیگر را هم در Viewهای تولیدی حتما نیاز است که ویرایش کنیم. آن هم مرتبط است به لینک حذف اطلاعات یک کاربر در صفحه Index.cshtml:
@Html.ActionLink("Delete", "Delete", new { id=item.Id }
در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار میدهیم تا اطلاعات به سرور Post شوند:
@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}
در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال میکند، مشاهده میکنید.
علت وجود دو متد، به ازای هر Edit یا Create
به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کردهایم. کار متدهای HttpGet نمایش Viewهای متناظر به کاربر هستند. بنابراین وجود آنها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، میتوان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته میشود.
بررسی معتبر بودن اطلاعات دریافتی
کلاس پایه Controller که کنترلرهای برنامه از آن مشتق میشوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid میتوان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false میباشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.
شبیه سازی عملکرد ViewState در ASP.NET MVC
در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده میشود. چرا؟
صفحات وب، زمانیکه به سرور ارسال میشوند، تمام اطلاعات کنترلهای خود را از دست خواهد داد (صفحه پاک میشود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک میگیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامههای دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا میبرد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت میدهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترلها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed میباشد. در این حالت فریم ورک میداند که اطلاعات را چگونه به کنترلهای قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده میشود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شدهاند؛ که ... اصلا قابل قبول نیست و فوقالعاده برنامه را غیرحرفهای نمایش میدهد.
خطاهای نمایش داده شده به کاربر
به صورت پیش فرض خطایی که به کاربر نمایش داده میشود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. میتوان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرمهای تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:
@Html.ValidationSummary(true)
در اینجا خطاهای عمومی در سطح مدل نمایش داده میشوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:
ModelState.AddModelError(string.Empty, "There is something wrong with model.");
همچنین در این فرمها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)
EditorFor سعی میکند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش میدهد. همچنین بر اساس متادیتا یک خاصیت نیز میتواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال میشود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش میدهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته میشود. بحث بیشتر دربارهای این موارد به قسمتی مجزا و مستقل موکول میگردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام میدهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.
مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders
استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی میکنیم. فریم ورک هم در ادامه سعی میکند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترلهای موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار سادهتر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد میتواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافتهاند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی میتواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind میتوان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:
public ActionResult Edit([Bind(Include = "Name, Password")] User user)
در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت میشوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز میتوان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را میپذیرند (اعمال دستی model binding):
[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}
ب) ایجاد لیست سیاه
به همین ترتیب میتوان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)
در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا میتوان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.
لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم میتوان اعمال کرد. برای مثال:
using System;
using System.Web.Mvc;
namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}
این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا میتوان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;
namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}
در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.
ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاسهای ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل میکنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View میباشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل میشود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطهای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته میکند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاسهایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آنها قرار نمیگیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار میگیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.
ViewModel چگونه پیاده سازی میشود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه میکنند:
public class MyViewModel
{
public SomeDomainModel1 Model1 { get; set; }
public SomeDomainModel2 Model2 { get; set; }
...
}
یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. اینها را به این شکل محصور و کپسوله میکنیم. اگر View، واقعا به تمام فیلدهای این کلاسها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ میشود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاسهای تعریف شده. به عبارتی این نوع تعریف صحیح است:
public class MyViewModel
{
public string SomeExtraField1 { get; set; }
public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
public string PersonFullName { set; set; }
}
در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استانها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).
با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:
namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}
به این ترتیب دیگر خاصیت IsAdming اضافهای وجود ندارد که بخواهد مورد حمله واقع شود.
استفاده از model binding برای آپلود فایل به سرور
برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:
[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}
[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}
View متناظر هم میتواند به شکل زیر باشد:
@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}
اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده میشود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگیها و خاصیتهایی را میتوان تنظیم کرد، اجازه تعریف آنها را به صورت anonymously typed objects میسر کردهاند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرضها نیز anonymously typed object است. یا نمونه دیگر آنرا در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.
سفارشی سازی model binder پیش فرض ASP.NET MVC
در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت میکنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع میکنیم. توسط bindingContext.ValueProvider میتوان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آنرا تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونهای از این پیاده سازی را در ادامه ملاحظه میکنید:
using System;
using System.Globalization;
using System.Web.Mvc;
namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را
ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران میتوانند تاریخها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آنرا به صورت سراسری در تمام مدلها و اکشنمتدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند میتوان به روش زیر عمل کرد:
public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)
همچنین ویژگی ModelBinder را به یک کلاس هم میتوان اعمال کرد:
[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{