ASP.NET MVC #7
+ در متن ذکر شده: «عباراتی که پس از علامت @ معرفی میشوند، به صورت پیش فرض Html Encoded هستند (در قسمت 5 در اینباره بیشتر توضیح داده شد)». در قسمت پنجم «یک نکته امنیتی» و متد Html.Raw را مطالعه کنید.
<script src="~/scripts/jquery.filedrop.js" type="text/javascript"></script>
<div id="dropZone">فایل برنامه را به داخل این کادر بکشانید</div> <br> فایل یا فایلهای آپلود شده: <ul id="uploadResult"></ul>
.files { min-height: 42px; background: #CCC none repeat scroll 0% 0%; border-top: 1px solid #FFF; margin: 11px 0px; padding: 11px 13px; border-radius: 6px; } #dropZone.mouse-over { background-color: #1d4257; }
$('#dropZone').filedrop({ url: uploadAddress, paramname: 'files', maxFiles: 1, dragOver: function() { $('#dropZone').addClass('mouse-over'); }, dragLeave: function() { $('#dropZone').removeClass('mouse-over'); }, drop: function() { $('#dropZone').removeClass('mouse-over'); }, afterAll: function() { $('#dropZone').html('آپلود با موفقیت انجام شد'); }, uploadFinished: function(i, file, response, time) { $('#uploadResult').append('<li>' + file.name + '</li>'); } });
Url | آدرسی که قرار است فایلها به آن سمت ارسال شوند. |
Paramname | در سمت سرور باید فایلها را با استفاده از این نام پارامتر دریافت کنید. |
maxFiles | تعداد فایلهایی که میتوان با درگ و دراپ کردن روی آن به دست آورد. در بالا به یک فایل محدود شده است. |
dragOver | این رویداد زمانی اجرا خواهد شد که اشاره گر با حالت درگ کرده فایلها را به محل آپلود آورده است. |
dragLeave | موقعی که ماوس از محل آپلود خارج میشود |
drop | موقعی که شما فایلها را روی محل آپلود رها میکنید. |
afterAll | بعد از اینکه همه کارها تمام شد اجرا میشود.(آخرین رویداد) |
uploadFinished | کار آپلود به پایان رسیده است. در مثال بالا پس از پایان آپلود، نام فایل آپلود شده را به کاربر نشان دادهایم. |
نحوهی دریافت آن در سمت سرور, در یک اکشن متد به صورت زیر است:
[HttpPost] public virtual ActionResult UpdateApp(IEnumerable<HttpPostedFileBase>files) { foreach (HttpPostedFileBase file in files) { string filePath = Path.Combine(TempPath, file.FileName); file.SaveAs(filePath); } return Json(new {state = "success", message = "با موفقیت عملیات ارسال فایل انجام شد"}, JsonRequestBehavior.AllowGet); }
در اکشن متد بالا ما فایلها را از طریق نام پارامتر files که مشخص کرده بودیم، به عنوان یک لیست شمارشی دریافت میکنیم. کدها بالا برای سادهترین راه اندازی ممکن کفایت میکنند.
این موارد از اصلیترینها هستند که به کار میآیند. به غیر اینها یک سری خصوصیات اضافهتری هم برای آن وجود دارد.
fallback_id | اگر دوست دارید این آپلودر را نیر به یک آپلودر معمولی اتصال دهید از این شناسه استفاده کنید. |
withCredentials | با استفاده از کوکیها یک درخواست cross-origin ایجاد میکند. |
data | اگر دوست دارید به همراه فایلها اطلاعات دیگری هم به همراه آن
ارسال و پست شوند از این طریق اقدام نمایید. میتواند در قالب یک متغیر
باشد یا خروجی یک تابع.data: { param1: 'value1', param2: function(){ return calculated_data; } |
headers | برای ارسال مقدار اضافهتر در هدر درخواست به کار میرود و صدا زدن آن همانند کد data میباشد. |
error | در صورتیکه در فرایند آپلود خطایی رخ دهد، اجرا میگردد. نحوهی کدنویسی آن و بررسی خطاهای آن به شرح زیر است:error: function(err, file) { switch(err) { case 'BrowserNotSupported': alert('مرورگر از این فناوری پشتیبانی نمیکند') break; case 'TooManyFiles': // قصد آپلود همزمان فایلهای بیشتری از حد مجاز تعیین شده دارید break; case 'FileTooLarge': //حداقل حجم یکی از فایلها از حجم مجاز تعیین شده بیشتر است //برای دسترسی به نام آن فایل از کد زیر استفاده کنید //file.name break; case 'FileTypeNotAllowed': // نوع حداقل یکی از فایلها با نوعها مشخص شده ما یکی نیست break; case 'FileExtensionNotAllowed': // پسوند حداقل یکی از فایلها مورد تایید نیست break; default: break; } } |
allowedfiletypes | نوع فایلهای مجاز را تعیین میکند:allowedfiletypes: |
allowedfileextensions | پسوند فایل هایی که برای آپلود مجاز هستند را معرفی میکند.allowedfileextensions: |
maxfilesize | حداکثر حجم مجاز برای هر فایل که به مگابایت بیان میشود. |
docOver | این رویداد زمانی اجرا میشود که فایلهای درگ شده شما وارد محیط یا پنجره مرورگر میشود. |
uploadStarted | این رویداد زمانی اجرا میگردد که فرایند آپلود هر فایل به طور جداگانه در حال آغاز شدن است: متغیر i در کد زیر شامل اندیس فایلی است که آپلودش آغاز شده است و این اندیس از صفر آغاز میشود. متغیر file دسترسی شما را به اطلاعات یک فایل باز میکند مانند نام فایل. متغیر len تعداد فایل هایی را که کاربر در محل آپلود رها کرده است، باز میگرداند. function(i, file, len){ }, |
uploadFinished | با اتمام آپلود هر فایل، این رویداد فراخوانی میگردد. دو
پارامتر اول آن، همانند سابق هستند. پارامتر response خروجی json ایی را که در سمت
سرور برگرداندیم، به ما باز میگرداند. پارامتر بعدی، زمانی را که برای
آپلود طول کشیده است، بر میگرداند. function(i, file, response, time) { } |
progressUpdated | این رویداد برای نمایش پیشرفت یک آپلود مناسب است که آخرین پارامتر آن یک عدد صحیح از پیشرفت فایل را بر میگرداند.function(i, file, progress) { }, |
globalProgressUpdated | این رویداد میزان پیشرفت کلیه فایلها را به درصد باز میگرداند:function(progress) { $('#progress div') |
speedUpdated | سرعت آپلود هر فایل را با کیلوبیت بر ثانیه مشخص میکند.function(i, file, speed) { } |
rename | در صورتی که قصد تغییر نام فایل ارسالی را دارید میتوانید از این رویداد استفاده کنید. پارامتر name، نام اصلی فایل را بر میگرداند که میتوانید آن را دستکاری کنید و نام جدیدی را به عنوان خروجی برگردانید. نمونه کاربردی از این رویداد rename: function(name) { } |
beforeEach | این رویداد قبل از آپلود هر فایل آغاز میگردد و برگرداندن مقدار false در آن باعث جلوگیری و کنسل شدن آپلود آن فایل میگردد.function(file) { } |
beforeSend | پارامترهای اولی تکراری هستند ولی آخرین پارامتر یک
تابع done را میتوان به آن پاس کرد که قبل از اجرای کل عملیات آپلود صدا
زده میشود.function(file, i, done) { } |
PlUpload
DropZoneJS
این کتابخانه به نسبت DropFile امکانات بیشتری را دارد و در سایت اختصاصی آن مثالها و مستندات خوبی قرار گرفته است. در سادهترین حالت آن ابتدا فایل کتابخانه را صدا زده و سپس تگ فرم را به آن نسبت دهید:
<script src="https://rawgit.com/enyo/dropzone/master/dist/dropzone.js"></script> <form action="/upload-target" class="dropzone"></form>
Install-Package dropzone
با نصب این کتابخانه یک سری فایل CSS هم به سیستم اضافه میشود که میتوانید برای استایل دهی هر چه بیشتر از آن بهره ببرید. کد فرم را به شکل زیر تغییر دهید:
<form action="~/Home/SaveUploadedFile" method="post" enctype="multipart/form-data" class="dropzone" id="dropzoneForm" style="width: 50px; background: none; border: none;"> <div class="fallback"> <input name="file" type="file" multiple /> <input type="submit" value="Upload" /> </div> </form>
var myDropzone = new Dropzone("div#myId", { url: "/file/post"}); //============ OR ==================== $("div#myId").dropzone({ url: "/file/post" });
Dropzone.options.myId= { paramName: "file", //نام پارامتری که فایل از طریق آن انتقال میبابد maxFilesize: 2, // MB accept: function(file, done) { if (file.name == "justinbieber.jpg") { done("Naha, you don't."); } else { done(); } } };
یک نکته تکمیلی در مورد آپلود: در ASP.net به طور پیش فرض نهایت حجم فایل آپلودی 4 مگابایتی تعیین شده است که میتوانید آن را از طریق web.config تغییر دهید:
<configuration> <system.web> <httpRuntime maxRequestLength="1048576" /> </system.web> </configuration>
<system.webServer> <security> <requestFiltering> <requestLimits maxAllowedContentLength="1073741824" /> </requestFiltering> </security> </system.webServer>
تا اینجا در قسمت سوم، نحوهی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کنندهی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننتها ارتباط برقرار کرد؟
تهیه کامپوننت نمایش ستارهای امتیازهای محصولات
مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت میخواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستارهای آنها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرندهی آن دریافت کند. همچنین میخواهیم اگر کاربر بر روی این ستارهها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شدهاست:
<td>{{ product.starRating }}</td>
با توجه به اینکه کامپوننت نمایش ستارهای امتیازها، قابلیت استفادهی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آنرا به خارج از پوشهی products فعلی منتقل کنیم. برای مثال میتوان پوشهی app\shared را برای آن و تمامی کامپوننتهای با قابلیت استفادهی مجدد ایجاد کرد.
برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core'; @Component({ selector: 'ai-star', templateUrl: 'app/shared/star.component.html', styleUrls: ['app/shared/star.component.css'] }) export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>(); ngOnChanges(): void { this.starWidth = this.rating * 86 / 5; } onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); } }
سپس مسیر template و مسیر فایل css ویژهی آن، در تزئین کنندهی Component مشخص شدهاند. محتوای کامل این دو فایل را در ذیل مشاهده میکنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'> <div style="width: 86px"> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> </div> </div>
.crop { overflow: hidden; } div { cursor: pointer; }
معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده میشود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه میشود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستارهها است.
مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد
متد ngOnChanges، تنها به خواص ویژهای به نام «input properties» واکنش نشان میدهد. اگر یک کامپوننت تو در توی قرار گرفتهی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژهای به نام ()Input@ انجام میشود.
به همین جهت است که پیش از خاصیت rating در کلاس StarComponent، شاهد درج مزین کنندهی ویژهی ()Input@ هستیم:
export class StarComponent implements OnChanges { @Input() rating: number;
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی میکند:
[rating]='product.starRating'
بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشدهاست، قابلیت property binging فوق را در کامپوننت والد، ندارد.
اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که میتوان از این مقدار تغییر یافته، جهت ترجمهی آن به عرض div نمایش ستارهها، استفاده کرد.
ارسال دادهها از کامپوننت فرزند به کامپوننت والد
تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژهی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا میکند:
export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در مثال جاری اگر کاربر بر روی div ستارههای نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام میشود (متد جدید onClick به رخداد click متصل شدهاست):
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'>
onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); }
تا اینجا مرحلهی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کنندهی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.
اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شدهی با Output، به عنوان مقصد event binding مشخص میشود و سپس متدی در کلاس کامپوننت والد، به آن متصل میگردد:
(ratingClicked)='onRatingClicked($event)'
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه میکنیم:
onRatingClicked(message: string): void { this.pageTitle = 'Product List: ' + message; }
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستارهای امتیازها، خاصیت pageTitle درج شدهی در صفحه تغییر میکند.
استفاده از کامپوننت نمایش ستارهای امتیازها
نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شدهی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشهی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر میدهیم:
<td> <ai-star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'> </ai-star> </td>
سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی میکنیم:
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; import { StarComponent } from '../shared/star.component'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe], directives: [StarComponent] })
نمونهای از اجرای برنامه را در تصویر ذیل مشاهده میکنید:
در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستارهای این امتیازها جایگزین شدهاست و همچنین با کلیک بر روی یکی از آنها، عنوان panel جاری تغییر کردهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part6.zip
خلاصهی بحث
در اینجا نحوهی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف میشوند، میدان دید آنها محدود است به قالب تعریف شدهی متناظر با آنها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرندهی دیگری در دسترس قرار گیرد، آنرا با مزین کنندهی ()Input@ مشخص میکنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، اینکار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام میدهد. نوع آرگومان جنریک EventEmitter، تعیین کنندهی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده میشود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال میگردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده میشود. در اینجا هر رخدادی که توسط مزین کنندهی ()Output@ تعریف شده باشد، میتواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، میتوان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
آیا میدانید CSS سایت شما تا چه اندازه مفید و مورد مصرف بوده و کدامیک از selector های آن بدون مصرف باقیماندهاند؟
خوشبختانه افزونه مفیدی برای فایرفاکس به نام Dust-Me Selectors موجود است که خروجی سایت را بررسی کرده و اضافات را گوشزد خواهد کرد. این افزونه را از آدرس زیر میتوانید دریافت کنید:
https://addons.mozilla.org/en-US/firefox/addon/5392
پس از نصب، یک آیکون جارو به status bar فایرفاکس اضافه خواهد شد که با کلیک بر روی آن، صفحه جاری آنالیز شده و css selectors بدون استفاده در آن گوشزد خواهند شد.
همچنین مورد دیگری که عموما ردیابی آن مشکل است، تشخیص تصاویر مفقود یک صفحه است. کدامیک از عناصری که در فایل HTML نهایی به آنها ارجاعی وجود دارد واقعا در سایت ما موجود است و از قلم نیفتاده است؟
برای این منظور ابتدا Firebug را نصب کنید. سپس افزونه Yslow آنرا نیز باید نصب نمود.
زمانیکه یک صفحه درحال بارگذاری است، بر روی آیکون Yslow در status bar فایرفاکس کلیک کرده و پس از نمایان شدن آن، بر روی Performance کلیک کنید تا کار آنالیز عناصر صفحه آغاز شود. پس از پایان کار، بر روی دکمه components کلیک نمائید تا علاوه بر مشاهده تاثیر عناصر مختلف صفحه بر نحوه بارگذاری و سرعت سایت شما، عناصر مفقود را با رنگ قرمز نمایان سازد.
این مثال شبیه به مثال بررسی وجود نام کاربر با استفاده از jQuery Ajax است که از ذکر توضیحات مشابه آن، در اینجا خودداری خواهد شد.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestBrokenImages.aspx.cs"
Inherits="testWebForms87.TestBrokenImages" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>detecting broken images</title>
<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
function errorReplace(arg) {
//ارسال پیغام خطا
$.ajax({
type: "POST",
url: "TestBrokenImages.aspx/GetErros",
data: "{'image': '" + arg.src + "','page':'" + location.href + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json"
});
//نمایش تصویری دلخواه بجای نمونه مفقود
$(arg).attr('src', 'missing.png');
}
//بررسی وضعیت تک تک تصاویر پس از بارگذاری کامل صفحه
$(document).ready(function() {
$(window).bind('load', function() {
$('img').each(function() {
if (!this.complete || (!$.browser.msie && (typeof this.naturalWidth == "undefined" || this.naturalWidth == 0))) {
errorReplace(this);
}
});
})
});
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<img src="img1.png" />
<img src="img2.png" />
</div>
</form>
</body>
</html>
using System;
using System.IO;
using System.Web.Services;
namespace testWebForms87
{
public partial class TestBrokenImages : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
}
[WebMethod]
public static void GetErros(string image,string page)
{
//ارسال ایمیل به مسؤول سایت و یا ذخیره خطاها در دیتابیس
}
}
}
در این مثال زمانیکه صفحه کاملا بارگذاری شد، وضعیت تک تک تصاویر بررسی میشود، اگر تصویر مفقودی وجود داشت (با اکثر مرورگرها سازگار است)، اطلاعات آن به تابع errorReplace ارسال خواهد شد.
در این تابع با استفاده از jQuery Ajax ، اطلاعات تصویر مفقود و صفحه مربوطه به وب متد GetErros ما ارسال میشود. سپس در این متد میتوان یا آرگومانهای دریافتی را به صورت یک ایمیل به مسؤول سایت ارسال نمود و یا آنها را جهت بررسی آتی در یک دیتابیس ذخیره کرد.
بدیهی است بجای قرار دادن وب متد فوق در صفحه جاری، میتوان یک وب سرویس را نیز ایجاد و متد را در آن قرار داد تا نیازی نباشد به ازای هر صفحه سایت یکبار این متد تکرار شود.
اگر موفق به اجرای این مثال نشدید، برای مثال یک break point داخل متد GetErrors قرار دهید و برنامه را در حالت دیباگ در ویژوال استودیو شروع کنید، اگر اتفاق خاصی رخ نداد و به این break point نرسیدید، احتمالا تنظیمات وب کانفیگ شما مناسب نیست. قسمت مربوط به system.web.extensions ، webServices و jsonSerialization باید در وب کانفیگ موجود باشد که VS 2008 این موارد را به صورت خودکار اضافه میکند.
ORM های NHibernate و Entity framework روشهای متفاوتی را برای به روز رسانی کلید خارجی با حداقل رفت و برگشت به دیتابیس ارائه میدهند که در ادامه معرفی خواهند شد.
صورت مساله:
فرض کنید میخواهیم برنامهای را بنویسیم که ریز پرداختهای روزانهی ما را ثبت کند. برای اینکار حداقل به یک جدول گروههای اقلام خریداری شده، یک جدول حسابهای تامین کنندهی مخارج، یک جدول فروشندهها و نهایتا یک جدول صورتحسابهای پرداختی بر اساس جداول ذکر شده نیاز خواهد بود.
الف) بررسی مدل برنامه
در اینجا جهت تعریف ویژگیها یا Attributes تعریف شده در این کلاسها از NHibernate validator استفاده شده (+). مزیت اینکار هم علاوه بر اعتبارسنجی سمت کلاینت (پیش از تبادل اطلاعات با بانک اطلاعاتی)، تولید جداولی با همین مشخصات است. برای مثال Fluent NHibernate بر اساس ویژگی Length تعریف شده با طول حداکثر 120 ، یک فیلد nvarchar با همین طول را ایجاد میکند.
public class Account
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Category
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 130, Message = "طول نام باید بین 3 و 130 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Payee
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}
public class Bill
{
public virtual int Id { get; set; }
[NotNull]
public virtual Account Account { get; set; }
[NotNull]
public virtual Category Category { get; set; }
[NotNull]
public virtual Payee Payee { get; set; }
[NotNull]
public virtual decimal Amount { set; get; }
[NotNull]
public virtual DateTime BillDate { set; get; }
[NotNullNotEmpty]
[Length(Min = 1, Max = 500, Message = "طول توضیحات باید بین 1 و 500 کاراکتر باشد")]
public virtual string Description { get; set; }
}
ب) ساختار جداول متناظر (تولید شده به صورت خودکار توسط Fluent NHibernate در اینجا)
در مورد نحوهی استفاده از ویژگی AutoMapping و همچنین تولید خودکار ساختار بانک اطلاعاتی از روی جداول در NHibernate قبلا توضیح داده شده است. البته بدیهی است که ترکیب مقالهی Validation و آشنایی با AutoMapping در اینجا جهت اعمال ویژگیها باید بکار گرفته شود که در همان مقالهی Validation مفصل توضیح داده شده است.
نکتهی مهم database schema تولیدی، کلیدهای خارجی (foreign key) تعریف شده بر روی جدول Bills است (همان AccountId، CategoryId و PayeeId تعریف شده) که به primary key جداول متناظر اشاره میکند.
create table Accounts (
AccountId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (AccountId)
)
create table Bills (
BillId INT IDENTITY NOT NULL,
Amount DECIMAL(19,5) not null,
BillDate DATETIME not null,
Description NVARCHAR(500) not null,
AccountId INT not null,
CategoryId INT not null,
PayeeId INT not null,
primary key (BillId)
)
create table Categories (
CategoryId INT IDENTITY NOT NULL,
Name NVARCHAR(130) not null,
primary key (CategoryId)
)
create table Payees (
PayeeId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (PayeeId)
)
alter table Bills
add constraint fk_Account_Bill
foreign key (AccountId)
references Accounts
alter table Bills
add constraint fk_Category_Bill
foreign key (CategoryId)
references Categories
alter table Bills
add constraint fk_Payee_Bill
foreign key (PayeeId)
references Payees
ج) صفحهی ثبت صورتحسابها
صفحات ثبت گروههای اقلام، حسابها و فروشندهها، نکتهی خاصی ندارند. چون این جداول وابستگی خاصی به جایی نداشته و به سادگی اطلاعات آنها را میتوان ثبت یا به روز کرد.
صفحهی مشکل در این مثال، همان صفحهی ثبت صورتحسابها است که از سه کلید خارجی به سه جدول دیگر تشکیل شده است.
عموما برای طراحی این نوع صفحات، کلیدهای خارجی را با drop down list نمایش میدهند و اگر در جهت سهولت کار کاربر قدم برداشته شود، باید از یک Auto complete drop down list استفاده کرد تا کاربر برنامه جهت یافتن آیتمهای از پیش تعریف شده کمتر سختی بکشد.
اگر از Silverlight یا WPF استفاده شود، امکان بایند یک لیست کامل از اشیاء با تمام خواص مرتبط به آنها وجود دارد (هر رکورد نمایش داده شده در دراپ داون لیست، دقیقا معادل است با یک شیء متناظر با کلاسهای تعریف شده است). اگر از ASP.NET استفاده شود (یعنی یک محیط بدون حالت که پس از نمایش یک صفحه دیگر خبری از لیست اشیاء بایند شده وجود نخواهد داشت و همگی توسط وب سرور جهت صرفه جویی در منابع تخریب شدهاند)، بهتر است datatextfield را با فیلد نام و datavaluefield را با فیلد Id مقدار دهی کرد تا کاربر نهایی، نام را جهت ثبت اطلاعات مشاهده کند و برنامه از Id موجود در لیست جهت ثبت کلیدهای خارجی استفاده نماید.
و نکتهی اصلی هم همینجا است که چگونه؟! چون ما زمانیکه با یک ORM سر و کار داریم، برای ثبت یک رکورد در جدول Bills باید یک وهله از کلاس Bill را ایجاد کرده و خواص آنرا مقدار دهی کنیم. اگر به تعریف کلاس Bill مراجعه کنید، سه خاصیت آن از نوع سه کلاس مجزا تعریف شده است. به به عبارتی با داشتن فقط یک id از رکوردهای این کلاسها باید بتوان سه وهلهی متناظر آنها را از بانک اطلاعاتی خواند و سپس به این خواص انتساب داد:
var newBill = new Bill
{
Account = accountRepository.GetByKey(1),
Amount = 1,
BillDate = DateTime.Now,
Category = categoryRepository.GetByKey(1),
Description = "testestest...",
Payee = payeeRepository.GetByKey(1)
};
- یکبار برای دریافت رکورد متناظر با گروهها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار برای دریافت رکورد متناظر با فروشندهها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار برای دریافت رکورد متناظر با حسابها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت میشود)
- یکبار هم ثبت نهایی اطلاعات در بانک اطلاعاتی
متد GetByKey فوق همان متد session.Get استاندارد NHibernate است (چون به primary key ها از طریق drop down list دسترسی داریم، به سادگی میتوان بر اساس متد Get استاندارد ذکر شده عمل کرد).
SQL نهایی تولیدی هم به صورت واضحی این مشکل را نمایش میدهد (4 بار رفت و برگشت؛ سه بار select یکبار هم insert نهایی):
SELECT account0_.AccountId as AccountId0_0_, account0_.Name as Name0_0_
FROM Accounts account0_ WHERE account0_.AccountId=@p0;@p0 = 1 [Type: Int32 (0)]
SELECT category0_.CategoryId as CategoryId2_0_, category0_.Name as Name2_0_
FROM Categories category0_ WHERE category0_.CategoryId=@p0;@p0 = 1 [Type: Int32 (0)]
SELECT payee0_.PayeeId as PayeeId3_0_, payee0_.Name as Name3_0_
FROM Payees payee0_ WHERE payee0_.PayeeId=@p0;@p0 = 1 [Type: Int32 (0)]
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:48:33 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]
کسانی که قبلا با رویههای ذخیره شده کار کرده باشند (stored procedures) احتمالا الان خواهند گفت؛ ما که گفتیم این روش کند است! سربار زیادی دارد! فقط کافی است یک SP بنویسید و کل عملیات را با یک رفت و برگشت انجام دهید.
اما در ORMs نیز برای انجام این مورد در طی یک حرکت یک ضرب راه حلهایی وجود دارد که در ادامه بحث خواهد شد:
د) پیاده سازی با NHibernate
برای حل این مشکل در NHibernate با داشتن primary key (برای مثال از طریق datavaluefield ذکر شده)، بجای session.Get از session.Load استفاده کنید.
session.Get یعنی همین الان برو به بانک اطلاعاتی مراجعه کن و رکورد متناظر با کلید اصلی ذکر شده را بازگشت بده و یک شیء از آن را ایجاد کن (حالتهای دیگر دسترسی به اطلاعات مانند استفاده از LINQ یا Criteria API یا هر روش مشابه دیگری نیز در اینجا به همین معنا خواهد بود).
session.Load یعنی فعلا دست نگه دار! مگر در جدول نهایی نگاشت شده، اصلا چیزی به نام شیء مثلا گروه وجود دارد؟ مگر این مورد واقعا یک فیلد عددی در جدول Bills بیشتر نیست؟ ما هم که الان این عدد را داریم (به کمک عناصر دراپ داون لیست)، پس لطفا در پشت صحنه یک پروکسی برای ایجاد شیء مورد نظر ایجاد کن (uninitialized proxy to the entity) و سپس عملیات مرتبط را در حین تشکیل SQL نهایی بر اساس این عدد موجود انجام بده. یعنی نیازی به رفت و برگشت به بانک اطلاعاتی نیست. در این حالت اگر SQL نهایی را بررسی کنیم فقط یک سطر زیر خواهد بود (سه select ذکر شده حذف خواهند شد):
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:58:22 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]
ه) پیاده سازی با Entity framework
Entity framework زمانیکه بانک اطلاعاتی فوق را (به روش database first) به کلاسهای متناظر تبدیل/نگاشت میکند، حاصل نهایی مثلا در مورد کلاس Bill به صورت خلاصه به شکل زیر خواهد بود:
public partial class Bill : EntityObject
{
public global::System.Int32 BillId {set;get;}
public global::System.Decimal Amount {set;get;}
public global::System.DateTime BillDate {set;get;}
public global::System.String Description {set;get;}
public global::System.Int32 AccountId {set;get;}
public global::System.Int32 CategoryId {set;get;}
public global::System.Int32 PayeeId {set;get;}
public Account Account {set;get;}
public Category Category {set;get;}
}
افزودن strong typing به کامپوننت نمایش لیست محصولات
یکی از مزایای کار با TypeScript امکان انتساب نوعهای مشخص یا سفارشی، به متغیرها و اشیاء تعریف شدهاست. برای مثال تاکنون هر خاصیت تعریف شدهای دارای نوع است. اما هنوز نوعی را برای آرایهی محصولات تعریف نکردهایم و نوع آن، نوع پیش فرض any است که برخلاف رویهی متداول کار با TypeScript است.
برای تعریف نوعهای سفارشی میتوان از اینترفیسهای TypeScript استفاده کرد. یک اینترفیس، قراردادی است که نحوهی تعریف تعدادی خاصیت و متد به هم مرتبط را مشخص میکند. سپس کلاسهای مختلف میتوانند با پیاده سازی این اینترفیس، قرارداد تعریف شدهی در آن را عملی کنند. همچنین از اینترفیسها میتوان به عنوان یک data type جدید نیز استفاده کرد. البته ES 5 و ES 6 از اینترفیسها پشتیبانی نمیکنند و تعریف آنها در TypeScript صرفا جهت کمک به کامپایلر، برای یافتن خطاها، پیش از اجرای برنامه است و به کدهای جاوا اسکریپتی معادلی ترجمه نمیشوند.
در ادامه برای تکمیل مثال این سری، فایل جدید App\products\product.ts را به پروژه اضافه کنید؛ با این محتوا:
export interface IProduct { productId: number; productName: string; productCode: string; releaseDate: string; price: number; description: string; starRating: number; imageUrl: string; }
همچنین از آنجائیکه این اینترفیس را در یک فایل ts مجزا قرار دادهایم، برای اینکه بتوان از آن در سایر قسمتهای برنامه استفاده کرد، نیاز است در ابتدای آن، واژهی کلیدی export را نیز ذکر کرد.
پس از تعریف این اینترفیس، برای استفاده از آن به عنوان یک data type جدید، ابتدا ماژول آن import خواهد شد و سپس از نام آن به عنوان نوع دادهی جدیدی، استفاده میشود. برای این منظور فایل product-list.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { IProduct } from './product'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html' }) export class ProductListComponent { // as before ... products: IProduct[] = [ // as before ... ]; // as before ... }
مزیت اینکار این است که برای مثال در اینجا اگر در لیست اعضای آرایهی products، نام خاصیتی اشتباه تایپ شده باشد یا حتی بجای عدد، از رشته استفاده شده باشد، بلافاصله در ادیتور مورد استفاده، خطای مرتبط گوشزد شده و همچنین این فایل دیگر کامپایل نخواهد شد. به علاوه اینبار برای تعریف خواص اعضای آرایهی products، ادیتور مورد استفاده، intellisense را نیز دراختیار ما قرار میدهد و کاملا مشخص است که چه اعضایی مدنظر هستند و نوع آنها چیست.
مدیریت cssهای هر کامپوننت به صورت مجزا
هنگام ساخت یک قالب یا template، در بسیاری از اوقات نیاز است css مرتبط با آن نیز، منحصر به همان قالب بوده و نشتی نداشته باشد. برای مثال زمانیکه یک کامپوننت را درون کامپوننتی دیگر قرار میدهیم، باید css آن نیز در دسترس قرار بگیرد و css فعلی کامپوننت دربرگیرنده را بازنویسی نکند. روشهای مختلفی برای مدیریت این مساله وجود دارند:
الف) تعریف شیوه نامهها به صورت inline داخل خود قالبها. این حالت، مشکلات نگهداری و استفادهی مجدد را دارد.
ب) تعریف شیوه نامهها در یک فایل خارجی css و سپس لینک کردن آن به صفحهای اصلی یا index.html
در این حالت به ازای هر فایل، یکبار باید این تعریف در صفحهای اصلی سایت صورت گیرد. همچنین این فایلها میتوانند مقادیر یکدیگر را بازنویسی کرده و بر روی هم تاثیر بگذارند.
ج) تعریف شیوه نامهها به همراه تعریف کامپوننت. این روشی است که توسط AngularJS توصیه شدهاست و نگهداری و مقیاس پذیری آن سادهتر است.
تزئین کنندهی Component به همراه دو خاصیت دیگر به نامهای styles و stylesUrl نیز میباشد.
در حالت استفاده از خاصیت styles، شیوهنامهی متناظر با کامپوننت، در همانجا به صورت inline تعریف میشود:
@Component({ //... styles: ['thead {color: blue;}'] })
روش بهتر، استفاده از خاصیت styleUrls است که در آن میتوان مسیر یک یا چند فایل css را مشخص کرد:
@Component({ //... styleUrls: ['app/products/product-list.component.css'] })
برای آزمایش آن فایل جدید product-list.component.css را به پوشهی products مثال این سری اضافه کنید؛ با این محتوا:
thead { color: #337AB7; }
@Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'] }) export class ProductListComponent { //...
یک نکته
شیوه نامهای که به این صورت توسط AngularJS 2.0 اضافه میشود، با سایر شیوه نامههای موجود تداخل نخواهد کرد. علت آنرا در تصویر ذیل که با استفاده از developer tools مرورگرها قابل بررسی است، میتوان مشاهده کرد:
در اینجا AngularJS 2.0، با ایجاد ویژگیهای سفارشی خودکار (attributes) میدان دید css را کنترل میکند. به این ترتیب شیوه نامهی کامپوننت یک، که درون کامپوننت دو قرار گرفتهاست، نشتی نداشته و بر روی سایر قسمتهای صفحه تاثیری نخواهد گذاشت؛ برخلاف شیوه نامههایی که به صورت متداولی به صفحهی اصلی سایت لینک شدهاند.
بررسی چرخهی حیات کامپوننتها
هر کامپوننت دارای چرخهی حیاتی است که توسط AngularJS 2.0 مدیریت میشود و شامل مراحلی مانند ایجاد، رندر، ایجاد و رندر فرزندان آن، پردازش تغییرات آن و در نهایت تخریب آن کامپوننت میشود. برای اینکه بتوان با برنامه نویسی به این مراحل چرخهی حیات یک کامپوننت دسترسی یافت، تعدادی life cycle hook طراحی شدهاند. سه مورد از مهمترین life cycle hooks شامل موارد ذیل هستند:
الف) OnInit: از این hook برای انجام کارهای آغازین یک کامپوننت مانند دریافت اطلاعات از سرور، استفاده میشود.
ب) OnChanges: از آن جهت انجام اعمالی پس از تغییرات input properties استفاده میشود.
خواص ورودی و همچنین کار با سرور را در قسمتهای بعدی بررسی خواهیم کرد.
ج) OnDestroy: از آن جهت پاکسازی منابع اختصاص داده شده استفاده میشود.
برای استفادهی از این hookها، نیاز است اینترفیس آنها را پیاده سازی کنیم. از آنجائیکه AngularJS 2.0 نیز با TypeScript نوشته شدهاست، به همراه تعدادی اینترفیس از پیش تعریف شده میباشد. برای مثال به ازای هر life cycle hook، یک اینترفیس تعریف شده در آن وجود دارد. برای نمونه اینترفیس hook ایی به نام OnInit، دقیقا همان OnInit، نام دارد (و با I شروع نشدهاست):
export class ProductListComponent implements OnInit {
import { Component, OnInit } from 'angular2/core';
ngOnInit(): void { console.log('In OnInit'); }
به عنوان تمرین، فایل product-list.component.ts را گشوده و سه مرحلهی implements سپس import و در آخر تعریف متد ngOnInit فوق را به آن اضافه کنید.
در ادامه برنامه را اجرا کرده و به کنسول developer tools مرورگر خود جهت مشاهدهی console.log فوق مراجعه کنید:
ساخت یک Pipe سفارشی جهت فعال سازی textbox فیلتر کردن محصولات
همانطور که در قسمت قبل نیز عنوان شد، کار pipes، تغییر اطلاعات حاصل از data binding، پیش از نمایش آنها در رابط کاربری است و AngularJS 2.0 به همراه تعدادی pipe توکار است؛ مانند currency، percent و غیره. در ادامه قصد داریم یک pipe سفارشی را ایجاد کنیم تا بر روی حلقهی ngFor* نمایش لیست محصولات تاثیرگذار شود و همچنین ورودی خود را از مقدار وارد شدهی توسط کاربر دریافت کند.
برای این منظور، یک فایل جدید را به نام product-filter.pipe.ts به پوشهی products اضافه کنید. سپس کدهای آنرا به نحو ذیل تغییر دهید:
import { PipeTransform, Pipe } from 'angular2/core'; import { IProduct } from './product'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], args: string[]): IProduct[] { let filter: string = args[0] ? args[0].toLocaleLowerCase() : null; return filter ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filter) != -1) : value; } }
برای مثال در اینجا میخواهیم شرایط فیلتر محصولات وارد شدهی توسط کاربر را دریافت کنیم.
خروجی این متد نیز از نوع آرایهای از IProduct تعریف شدهاست؛ از این جهت که نتیجه نهایی فیلتر اطلاعات نیز آرایهای از همین نوع است. کار این pipe پیاده سازی متد contains به صورت غیرحساس به کوچکی و بزرگی حروف است.
سپس بلافاصله بالای نام این کلاس، از یک decorator جدید به نام Pipe استفاده شدهاست تا به AngularJS 2.0 اعلام شود، این کلاس، صرفا یک کلاس معمولی نیست و یک Pipe است.
در ابتدای فایل هم importهای لازم جهت تعریف اینترفیسهای مورد استفادهی در این ماژول، ذکر شدهاند.
اگر دقت کنید، الگوی ایجاد یک pipe جدید، بسیار شبیه است به الگوی ایجاد یک کامپوننت و از این لحاظ سعی شدهاست طراحی یک دستی در سراسر این فریم ورک بکار گرفته شود.
پس از تعریف این pipe سفارشی، برای استفادهی از آن در یک template، به فایل product-list.component.html مراجعه کرده و سپس ngFor* آنرا به نحو ذیل تغییر میدهیم:
<tr *ngFor='#product of products | productFilter:listFilter'>
اگر از قسمت قبل به خاطر داشته باشید، این خاصیت را توسط two-way binding به روز میکنیم (اطلاعات وارد شدهی در textbox، بلافاصله به این خاصیت منعکس میشوند و برعکس):
<input type='text' [(ngModel)]='listFilter' />
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe] }) export class ProductListComponent implements OnInit { //...
اکنون اگر برنامه را اجرا کنید، خروجی ذیل را مشاهده خواهید کرد:
در اینجا چون مقدار فیلتر وارد شدهی پیش فرض، cart است، فقط ردیف Garden Cart نمایش داده شدهاست. اگر این مقدار را خالی کنیم، تمام ردیفها نمایش داده میشوند و اگر برای مثال ham را جستجو کنیم، فقط ردیف Hammer نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part5.zip
خلاصهی بحث
- اینترفیسها یکی از روشهای بهبود strong typing برنامههای AngularJS 2.0 هستند.
- جهت مدیریت بهتر شیوهنامههای هر کامپوننت بهتر است از روش styleUrls استفاده شود تا از نشتیهای تعاریف شیوهنامهها جلوگیری گردد.
- از life cycle hooks برای مدیریت رخدادهای مرتبط با طول عمر یک کامپوننت استفاده میشود؛ برای مثال دریافت اطلاعات از سرور و یا پاکسازی منابع مصرفی.
- تعریف یک pipe سفارشی با پیاده سازی اینترفیس PipeTransform انجام میشود. سپس نام این Pipe، به قالب مدنظر اضافه شده و در ادامه نیاز است کامپوننت استفاده کنندهی از این قالب را نیز از وجود این Pipe مطلع کرد.
- با نصب و اجرای Visual Studio 2013 Express for Web یا Visual Studio 2013 شروع کنید.
- یک پروژه جدید بسازید (از صفحه شروع یا منوی فایل)
- گزینه #Visual C و سپس ASP.NET Web Application را انتخاب کنید. نام پروژه را به "WebFormsIdentity" تغییر داده و OK کنید.
- در دیالوگ جدید ASP.NET گزینه Empty را انتخاب کنید.
دقت کنید که دکمه Change Authentication غیرفعال است و هیچ پشتیبانی ای برای احراز هویت در این قالب پروژه وجود ندارد.
افزودن پکیجهای ASP.NET Identity به پروژه
دقت کنید که نصب کردن این پکیج وابستگیها را نیز بصورت خودکار نصب میکند: Entity Framework و ASP.NET Idenity Core.
افزودن فرمهای وب لازم برای ثبت نام کاربران
در دیالوگ باز شده نام فرم را به Register تغییر داده و تایید کنید.
فایل ایجاد شده جدید را باز کرده و کد Markup آن را با قطعه کد زیر جایگزین کنید.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Register.aspx.cs" Inherits="WebFormsIdentity.Register" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body style=" <form id="form1" runat="server"> <div> <h4 style="Register a new user</h4> <hr /> <p> <asp:Literal runat="server" ID="StatusMessage" /> </p> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label> <div> <asp:TextBox runat="server" ID="UserName" /> </div> </div> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label> <div> <asp:TextBox runat="server" ID="Password" TextMode="Password" /> </div> </div> <div style="margin-bottom:10px"> <asp:Label runat="server" AssociatedControlID="ConfirmPassword">Confirm password</asp:Label> <div> <asp:TextBox runat="server" ID="ConfirmPassword" TextMode="Password" /> </div> </div> <div> <div> <asp:Button runat="server" OnClick="CreateUser_Click" Text="Register" /> </div> </div> </div> </form> </body> </html>
این تنها یک نسخه ساده شده Register.aspx است که از چند فیلد فرم و دکمه ای برای ارسال آنها به سرور استفاده میکند.
فایل کد این فرم را باز کرده و محتویات آن را با قطعه کد زیر جایگزین کنید.
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Linq; namespace WebFormsIdentity { public partial class Register : System.Web.UI.Page { protected void CreateUser_Click(object sender, EventArgs e) { // Default UserStore constructor uses the default connection string named: DefaultConnection var userStore = new UserStore<IdentityUser>(); var manager = new UserManager<IdentityUser>(userStore); var user = new IdentityUser() { UserName = UserName.Text }; IdentityResult result = manager.Create(user, Password.Text); if (result.Succeeded) { StatusMessage.Text = string.Format("User {0} was created successfully!", user.UserName); } else { StatusMessage.Text = result.Errors.FirstOrDefault(); } } } }
کد این فرم نیز نسخه ای ساده شده است. فایلی که بصورت خودکار توسط VS برای شما ایجاد میشود متفاوت است.
کلاس IdentityUser پیاده سازی پیش فرض EntityFramework از قرارداد IUser است. قرارداد IUser تعریفات حداقلی یک کاربر در ASP.NET Identity Core را در بر میگیرد.
کلاس UserStore پیاده سازی پیش فرض EF از یک فروشگاه کاربر (user store) است. این کلاس چند قرارداد اساسی ASP.NET Identity Core را پیاده سازی میکند: IUserStore, IUserLoginStore, IUserClaimStore و IUserRoleStore.
کلاس UserManager دسترسی به APIهای مربوط به کاربران را فراهم میکند. این کلاس تمامی تغییرات را بصورت خودکار در UserStore ذخیره میکند.
کلاس IdentityResult نتیجه یک عملیات هویتی را معرفی میکند (identity operations).
پوشه App_Data را به پروژه خود اضافه کنید.
فایل Web.config پروژه را باز کنید و رشته اتصال جدیدی برای دیتابیس اطلاعات کاربران اضافه کنید. این دیتابیس در زمان اجرا (runtime) بصورت خودکار توسط EF ساخته میشود. این رشته اتصال شبیه به رشته اتصالی است که هنگام ایجاد پروژه بصورت خودکار برای شما تنظیم میشود.
<?xml version="1.0" encoding="utf-8"?> <!-- For more information on how to configure your ASP.NET application, please visit http://go.microsoft.com/fwlink/?LinkId=169433 --> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" /> </configSections> <connectionStrings> <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\WebFormsIdentity.mdf;Initial Catalog=WebFormsIdentity;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <system.web> <compilation debug="true" targetFramework="4.5" /> <httpRuntime targetFramework="4.5" /> </system.web> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework"> <parameters> <parameter value="v11.0" /> </parameters> </defaultConnectionFactory> <providers> <provider invariantName="System.Data.SqlClient" type="System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework.SqlServer" /> </providers> </entityFramework> </configuration>
همانطور که مشاهده میکنید نام این رشته اتصال DefaultConnection است.
روی فایل Register.aspx کلیک راست کنید و گزینه Set As Start Page را انتخاب کنید. اپلیکیشن خود را با کلیدهای ترکیبی Ctrl + F5 اجرا کنید که تمام پروژه را کامپایل نیز خواهد کرد. یک نام کاربری و کلمه عبور وارد کنید و روی Register کلیک کنید.
ASP.NET Identity از اعتبارسنجی نیز پشتیبانی میکند، مثلا در این مرحله میتوانید از اعتبارسنج هایی که توسط ASP.NET Identity Core عرضه میشوند برای کنترل رفتار فیلدهای نام کاربری و کلمه عبور استفاده کنید. اعتبارسنج پیش فرض کاربران (User) که UserValidator نام دارد خاصیتی با نام AllowOnlyAlphanumericUserNames دارد که مقدار پیش فرضش هم true است. اعتبارسنج پیش فرض کلمه عبور (MinimumLengthValidator) اطمینان حاصل میکند که کلمه عبور حداقل 6 کاراکتر باشد. این اعتبارسنجها بصورت propertyها در کلاس UserManager تعریف شده اند و میتوانید آنها را overwrite کنید و اعتبارسنجی سفارشی خود را پیاده کنید. از آنجا که الگوی دیتابیس سیستم عضویت توسط Entity Framework مدیریت میشود، روی الگوی دیتابیس کنترل کامل دارید، پس از Data Annotations نیز میتوانید استفاده کنید.
تایید دیتابیس LocalDbIdentity که توسط EF ساخته میشود
گره (DefaultConnection (WebFormsIdentity و سپس Tables را باز کنید. روی جدول AspNetUsers کلیک راست کرده و Show Table Data را انتخاب کنید.
پیکربندی اپلیکیشن برای استفاده از احراز هویت OWIN
نصب پکیجهای احراز هویت روی پروژه
به دنبال پکیجی با نام Microsoft.Owin.Host.SystemWeb بگردید و آن را نیز نصب کنید.
پکیج Microsoft.Aspnet.Identity.Owin حاوی یک سری کلاس Owin Extension است و امکان مدیریت و پیکربندی OWIN Authentication در پکیجهای ASP.NET Identity Core را فراهم میکند.
پکیج Microsoft.Owin.Host.SystemWeb حاوی یک سرور OWIN است که اجرای اپلیکیشنهای مبتنی بر OWIN را روی IIS و با استفاده از ASP.NET Request Pipeline ممکن میسازد. برای اطلاعات بیشتر به OWIN Middleware in the IIS integrated pipeline مراجعه کنید.
افزودن کلاسهای پیکربندی Startup و Authentication
فایل Startup.cs را باز کنید و قطعه کد زیر را با محتویات آن جایگزین کنید تا احراز هویت OWIN Cookie Authentication پیکربندی شود.
using Microsoft.AspNet.Identity; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; using Owin; [assembly: OwinStartup(typeof(WebFormsIdentity.Startup))] namespace WebFormsIdentity { public class Startup { public void Configuration(IAppBuilder app) { // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888 app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login") }); } } }
این کلاس حاوی خاصیت OwinAttribute است که کلاس راه انداز OWIN را نشانه گذاری میکند. هر اپلیکیشن OWIN یک کلاس راه انداز (startup) دارد که توسط آن میتوانید کامپوننتهای application pipeline را مشخص کنید. برای اطلاعات بیشتر درباره این مدل، به OWIN Startup Class Detection مراجعه فرمایید.
افزودن فرمهای وب برای ثبت نام و ورود کاربران
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; using System; using System.Linq; using System.Web; namespace WebFormsIdentity { public partial class Register : System.Web.UI.Page { protected void CreateUser_Click(object sender, EventArgs e) { // Default UserStore constructor uses the default connection string named: DefaultConnection var userStore = new UserStore<IdentityUser>(); var manager = new UserManager<IdentityUser>(userStore); var user = new IdentityUser() { UserName = UserName.Text }; IdentityResult result = manager.Create(user, Password.Text); if (result.Succeeded) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; var userIdentity = manager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(new AuthenticationProperties() { }, userIdentity); Response.Redirect("~/Login.aspx"); } else { StatusMessage.Text = result.Errors.FirstOrDefault(); } } } }
فایل Login.aspx را باز کنید و کد Markup آن را مانند قطعه کد زیر تغییر دهید.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="WebFormsIdentity.Login" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> </head> <body style="font-family: Arial, Helvetica, sans-serif; font-size: small"> <form id="form1" runat="server"> <div> <h4 style="font-size: medium">Log In</h4> <hr /> <asp:PlaceHolder runat="server" ID="LoginStatus" Visible="false"> <p> <asp:Literal runat="server" ID="StatusText" /> </p> </asp:PlaceHolder> <asp:PlaceHolder runat="server" ID="LoginForm" Visible="false"> <div style="margin-bottom: 10px"> <asp:Label runat="server" AssociatedControlID="UserName">User name</asp:Label> <div> <asp:TextBox runat="server" ID="UserName" /> </div> </div> <div style="margin-bottom: 10px"> <asp:Label runat="server" AssociatedControlID="Password">Password</asp:Label> <div> <asp:TextBox runat="server" ID="Password" TextMode="Password" /> </div> </div> <div style="margin-bottom: 10px"> <div> <asp:Button runat="server" OnClick="SignIn" Text="Log in" /> </div> </div> </asp:PlaceHolder> <asp:PlaceHolder runat="server" ID="LogoutButton" Visible="false"> <div> <div> <asp:Button runat="server" OnClick="SignOut" Text="Log out" /> </div> </div> </asp:PlaceHolder> </div> </form> </body> </html>
محتوای فایل Login.aspx.cs را نیز مانند لیست زیر تغییر دهید.
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; using System; using System.Web; using System.Web.UI.WebControls; namespace WebFormsIdentity { public partial class Login : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { if (User.Identity.IsAuthenticated) { StatusText.Text = string.Format("Hello {0}!", User.Identity.GetUserName()); LoginStatus.Visible = true; LogoutButton.Visible = true; } else { LoginForm.Visible = true; } } } protected void SignIn(object sender, EventArgs e) { var userStore = new UserStore<IdentityUser>(); var userManager = new UserManager<IdentityUser>(userStore); var user = userManager.Find(UserName.Text, Password.Text); if (user != null) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; var userIdentity = userManager.CreateIdentity(user, DefaultAuthenticationTypes.ApplicationCookie); authenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, userIdentity); Response.Redirect("~/Login.aspx"); } else { StatusText.Text = "Invalid username or password."; LoginStatus.Visible = true; } } protected void SignOut(object sender, EventArgs e) { var authenticationManager = HttpContext.Current.GetOwinContext().Authentication; authenticationManager.SignOut(); Response.Redirect("~/Login.aspx"); } } }
- متد Page_Load حالا وضعیت کاربر جاری را بررسی میکند و بر اساس وضعیت Context.User.Identity.IsAuthenticated تصمیم گیری میکند.
- متد SignIn
- پروژه را با Ctrl + F5 اجرا کنید و کاربر جدیدی بسازید. پس از وارد کردن نام کاربری و کلمه عبور و کلیک کردن دکمه Register باید بصورت خودکار به سایت وارد شوید و نام خود را مشاهده کنید.
- همانطور که مشاهده میکنید در این مرحله حساب کاربری جدید ایجاد شده و به سایت وارد شده اید. روی Log out کلیک کنید تا از سایت خارج شوید. پس از آن باید به صفحه ورود هدایت شوید.
- حالا یک نام کاربری یا کلمه عبور نامعتبر وارد کنید و روی Log in کلیک کنید.
در این قسمت قصد داریم یک فرم Ajaxایی را در ASP.NET MVC به همراه تمام مسایل اعتبارسنجی، پردازش اطلاعات و غیره را به کمک Twitter Bootstrap و jQuery Ajax پیاده سازی کنیم.
تهیه افزونه jquery.bootstrap-modal-ajax-form.js
از این جهت که مباحث مرتبط با نمایش و پردازش فرمهای مودال Ajaxایی به کمک Twitter Bootstrap اندکی نکته دار و طولانی هستند، بهتر است این موارد را به شکل یک افزونه، کپسوله کنیم. کدهای کامل افزونه jquery.bootstrap-modal-ajax-form.js را در ادامه ملاحظه میکنید:
// <![CDATA[ (function ($) { $.bootstrapModalAjaxForm = function (options) { var defaults = { renderModalPartialViewUrl: null, renderModalPartialViewData: null, postUrl: '/', loginUrl: '/login', beforePostHandler: null, completeHandler: null, errorHandler: null }; var options = $.extend(defaults, options); var validateForm = function (form) { //فعال سازی دستی اعتبار سنجی جیکوئری var val = form.validate(); val.form(); return val.valid(); }; var enableBootstrapStyleValidation = function () { $.validator.setDefaults({ highlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).addClass(errorClass).removeClass(validClass); } else { $(element).addClass(errorClass).removeClass(validClass); $(element).closest('.control-group').removeClass('success').addClass('error'); } $(element).trigger('highlited'); }, unhighlight: function (element, errorClass, validClass) { if (element.type === 'radio') { this.findByName(element.name).removeClass(errorClass).addClass(validClass); } else { $(element).removeClass(errorClass).addClass(validClass); $(element).closest('.control-group').removeClass('error').addClass('success'); } $(element).trigger('unhighlited'); } }); } var enablePostbackValidation = function () { $('form').each(function () { $(this).find('div.control-group').each(function () { if ($(this).find('span.field-validation-error').length > 0) { $(this).addClass('error'); } }); }); } var processAjaxForm = function (dialog) { $('form', dialog).submit(function (e) { e.preventDefault(); if (!validateForm($(this))) { //اگر فرم اعتبار سنجی نشده، اطلاعات آن ارسال نشود return false; } //در اینجا میتوان مثلا دکمهای را غیرفعال کرد if (options.beforePostHandler) options.beforePostHandler(); //اطلاعات نباید کش شوند $.ajaxSetup({ cache: false }); $.ajax({ url: options.postUrl, type: "POST", data: $(this).serialize(), success: function (result) { if (result.success) { $('#dialogDiv').modal('hide'); if (options.completeHandler) options.completeHandler(); } else { $('#dialogContent').html(result); if (options.errorHandler) options.errorHandler(); } } }); return false; }); }; var mainContainer = "<div id='dialogDiv' class='modal hide fade in'><div id='dialogContent'></div></div>"; enableBootstrapStyleValidation(); //اعمال نکات خاص بوت استرپ جهت اعتبارسنجی یکپارچه با آن $.ajaxSetup({ cache: false }); $.ajax({ type: "POST", url: options.renderModalPartialViewUrl, data: options.renderModalPartialViewData, contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; var data = xhr.responseText; if (xhr.status == 403) { window.location = options.loginUrl; //در حالت لاگین نبودن شخص اجرا میشود } else if (status === 'error' || !data) { if (options.errorHandler) options.errorHandler(); } else { var dialogContainer = "#dialogDiv"; $(dialogContainer).remove(); $(mainContainer).appendTo('body'); $('#dialogContent').html(data); // دریافت پویای اطلاعات مودال دیالوگ $.validator.unobtrusive.parse("#dialogContent"); // فعال سازی اعتبارسنجی فرمی که با ایجکس بارگذاری شده enablePostbackValidation(); // و سپس نمایش آن به صورت مودال $('#dialogDiv').modal({ backdrop: 'static', //با کلیک کاربر روی صفحه، صفحه مودال بسته نمیشود keyboard: true }, 'show'); // تحت نظر قرار دادن این فرم اضافه شده processAjaxForm('#dialogContent'); } } }); }; })(jQuery); // ]]>
- توابع enableBootstrapStyleValidation و enablePostbackValidation در مطلب «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» بررسی شدند.
- این افزونه با توجه به مقدار renderModalPartialViewUrl، یک partial view را از برنامه ASP.NET MVC درخواست میکند.
- سپس این partial view را به صورت خودکار به صفحه اضافه کرده و آنرا به صورت modal نمایش میدهد.
- پس از افزودن فرم Ajaxایی دریافتی، مسایل اعتبارسنجی را به آن اعمال کرده و سپس دکمه submit آنرا تحت کنترل قرار میدهد.
- در زمان submit، ابتدا بررسی میکند که آیا فرم معتبر است و اعتبارسنجی آن بدون مشکل است؟ اگر اینچنین است، اطلاعات فرم را به آدرس postUrl به صورت Ajaxایی ارسال میکند.
کدهای مدل برنامه
using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace Mvc4TwitterBootStrapTest.Models { public class User { public int Id { set; get; } [DisplayName("نام")] [Required(ErrorMessage="لطفا نام را تکمیل کنید")] public string Name { set; get; } [DisplayName("نام خانوادگی")] [Required(ErrorMessage = "لطفا نام خانوادگی را تکمیل کنید")] public string LastName { set; get; } } }
کدهای کنترلر برنامه
using System.Web.Mvc; using Mvc4TwitterBootStrapTest.Models; namespace Mvc4TwitterBootStrapTest.Controllers { public class ModalFormAjaxController : Controller { [HttpGet] public ActionResult Index() { return View(); //نمایش صفحه اولیه } [HttpPost] //برای این حالت امنتر است //[AjaxOnly] public ActionResult RenderModalPartialView() { //رندر پارشال ویوو صفحه مودال به همراه اطلاعات مورد نیاز آن return PartialView(viewName: "_ModalPartialView", model: new User { Name = "", LastName = "" }); } [HttpPost] //[AjaxOnly] public ActionResult Index(User user) //ذخیره سازی اطلاعات { if (this.ModelState.IsValid) { //todo: SaveChanges; return Json(new { success = true }); } this.ModelState.AddModelError("", "خطایی رخ داده است"); return PartialView("_ModalPartialView", user); } } }
الف) متد Index حالت HttpGet که صفحه ابتدایی را نمایش خواهد داد.
ب) متد RenderModalPartialView یک partial view اضافه شده به برنامه به نام _ModalPartialView.cshtml را بازگشت میدهد. این partial view در حقیقت همان فرمی است که قرار است به صورت مودال نمایش داده شود و پردازش آن نیز Ajaxایی باشد.
ج) متد Index حالت HttpPost که نهایتا اطلاعات فرم مودال را دریافت خواهد کرد. اگر پردازش موفقیت آمیز بود، نیاز است همانند کدهای فوق return Json صورت گیرد. در غیراینصورت مجددا همان partial view را بازگشت دهید.
کدهای Index.cshtml
@{ ViewBag.Title = "Index"; var renderModalPartialViewUrl = Url.Action("RenderModalPartialView", "ModalFormAjax"); var postDataUrl = Url.Action("Index", "ModalFormAjax"); } <h2> Index</h2> <a href="#" class="btn btn-primary" id="btnCreate">ثبت اطلاعات</a> @section JavaScript { <script type="text/javascript"> $(function () { $('#btnCreate').click(function (e) { e.preventDefault(); //میخواهیم لینک به صورت معمول عمل نکند $.bootstrapModalAjaxForm({ postUrl: '@postDataUrl', renderModalPartialViewUrl: '@renderModalPartialViewUrl', renderModalPartialViewData: {}, loginUrl: '/login', beforePostHandler: function () { }, completeHandler: function () { // Refresh: برای حالتیکه نیاز به به روز رسانی کامل صفحه زیرین باشد // location.reload(); }, errorHandler: function () { } }); }); }); </script> }
- در اینجا یک لینک ساده در صفحه قرار گرفته و به کمک کلاس btn مجموعه bootstrap به شکل یک دکمه مزین شده است.
- در ادامه نحوه استفاده از افزونهای را که در ابتدای بحث طراحی کردیم، ملاحظه میکنید. کار با آن بسیار ساده است و تنها باید مسیرهای ارسال اطلاعات نهایی به سرور یا postDataUrl و مسیر دریافت partial view رندر شده یا renderModalPartialViewUrl به آن معرفی شود. سایر مسایل آن خودکار است.
کدهای _ModalPartialView.cshtml یا همان فرم مودال برنامه
@model Mvc4TwitterBootStrapTest.Models.User <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"> ×</button> <h5> افزودن کاربر جدید</h5> </div> @using (Html.BeginForm("Index", " ModalFormAjax", FormMethod.Post, new { @class = "modal-form" })) { <div class="modal-body"> @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) <fieldset class="form-horizontal"> <legend>مشخصات کاربر</legend> <div class="control-group"> @Html.LabelFor(model => model.Name, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.Name) @Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" }) </div> </div> <div class="control-group"> @Html.LabelFor(model => model.LastName, new { @class = "control-label" }) <div class="controls"> @Html.EditorFor(model => model.LastName) @Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" }) </div> </div> </fieldset> </div> <div class="modal-footer"> <button class="btn btn-primary" type="submit"> ارسال</button> <button class="btn" data-dismiss="modal" aria-hidden="true"> انصراف</button> </div> }
حاصل نهایی این مبحث را در دو شکل ذیل ملاحظه میکنید. صفحه index نمایش دهنده یک دکمه و در ادامه باز شدن یک فرم مودال، پس از کلیک بر روی دکمه ثبت اطلاعات.