+ زمانیکه از DNTScheduler استفاده میکنید، عملا نیازی به QueueBackgroundWorkItem ندارید. چون نکتهی HostingEnvironment.RegisterObject و IRegisteredObject در آن لحاظ شده. این نکته که خاموش شدن IIS را گزارش میکند، چند سال قبل، توسط یکی از اعضای قبلی تیم ASP.NET منتشر شده بود. دقیقا از همین نکته در QueueBackgroundWorkItem استفاده شده.
به صورت خلاصه، DNTScheduler با دات نت 4 به بعد سازگار است و نکات QueueBackgroundWorkItem دات نت 4.5.2 را به صورت توکار پیاده سازی کردهاست.
- همچنین اگر هاست نامبرده تمام سایتها را با یک Application pool مدیریت میکند، کرش یکی از چند ده سایت دیگر میتواند سبب ریاستارت شدن سایت شما هم بشود؛ چون برنامهها از همه ایزوله نشدهاند. راه حل آن ایجاد یک Application pool مجزا به ازای هر سایت هست (توسط هاستدار).
برای ری استارت کردن یک برنامهی ASP.NET حتما نیازی نیست تا IIS را متوقف و سپس راه اندازی کرد یا تنظیمات App pool برنامه را در IIS تغییر داد. روشهای دیگری نیز وجود دارند که عدم آگاهی از آنها میتواند سبب بروز مشکلات عدیدهای گردد و گاها خطایابی آنها بسیار مشکل است؛ زیرا ری استارت شدن برنامه = از دست رفتن آنی تمام سشنهای InProc تمام کاربران سایت؛ پاک شدن کش برنامه در IIS؛ از دست رفتن تمام متغیرهای استاتیک، Application State و مواردی از این دست:
- - نوشتن در پوشه bin برنامه (ایجاد فایل یا ایجاد پوشه یا تغییر نام پوشهها و مواردی از این دست). (بنابراین قرار دادن بانک اطلاعاتی برنامه در این پوشه، اشتباه بوده و به همین جهت پوشهی استاندارد App_Data طراحی شده است)
- - نوشتن در فایل web.config برنامه (به صورت دستی، حتی در حد اضافه کردن یک فاصله یا توسط خود برنامه و یا استفاده از متد File.SetLastWriteTime). در این حالت ASP.NET FileSystemWatcher بلافاصله وارد عمل شده و برنامه را ری استارت میکند. (هدف اصلی وجودی این فایل، برخورد فقط خواندنی با آن است نه ذخیره سازی تنظیمات پویای برنامه در آن. برای ذخیره سازی تنظیمات پویا، از بانک اطلاعاتی و یا یک فایل XML مجزای از وب کانفیگ استفاده کنید یا مواردی مشابه)
- - هر گونه تغییری در فایلها و یا پوشههای App_WebReferences ، App_Code ، Global.asax ، machine.config ، App_GlobalResources و App_LocalResources نیز سبب ری استارت برنامه خواهند شد.
- - با ایجاد، حذف یا تغییر نام یکی از ساب دایرکتوریهای واقع شده در ریشه برنامه. بنابراین اگر برنامهی شما به صورت پویا پوشههایی را ایجاد یا حذف میکند باید منتظر ری استارتهای پی در پی باشید (البته این مورد با از کار انداختن FileChangesMonitor مربوط به HttpRuntime قابل حل میباشد (+)، ولی همانطور که عنوان شد به صورت پیش فرض همواره فعال است)
- - فراخوانی متد System.Web.HttpRuntime.UnloadAppDomain شبیه به همان Application.Exit در برنامههای دسکتاپ است و بلافاصله سبب خاتمهی برنامه میشود. قرار دادن فایل App_Offline.htm در پوشه اصلی برنامه نیز چنین رفتاری را سبب خواهد شد. علاوه بر آن تگ httpRuntime در وب کانفیگ نیز دارای گزینهی enable است و تنظیم آن به false ، سبب خاتمهی سرویس دهی برنامه خواهد شد.
- - رسیدن به عدد numRecompilesBeforeAppRestart تعریف شده در فایل machine.config که عموما به عدد 15 تنظیم شده است. اگر تغییرات زیادی را در فایلهای (مرتبط با ASP.NET مانند aspx ، asmx و غیره) برنامه داده باشید (بیشتر از 15 مورد) و نیازی به ری کامپایل اساسی وجود داشته باشد، ASP.NET FileSystemWatcher به صورت خودکار برنامه را ری استارت خواهد کرد.
ASP.NET MVC #24
مروری بر نمونه سؤالات ASP.NET MVC امتحانات مایکروسافت در چند سال اخیر
در قسمت آخر سری ASP.NET MVC بد نیست مروری داشته باشیم بر نمونه سؤالات امتحانات مایکروسافت؛ امتحانات 70-515 و 70-519 که در آنها تعدادی از سؤالات به ASP.NET MVC اختصاص دارند. در این سؤالات امکان انتخاب بیش از یک گزینه نیز وجود دارد.
1) شما در حال توسعه یک برنامهی ASP.NET MVC هستید. باید درخواست Ajax ایی از صفحهای صادر شده و خروجی زیر را از اکشن متدی دریافت کند:
["Adventure Works","Contoso"]
کدام نوع خروجی اکشن متد زیر را برای اینکار مناسب میدانید؟
a) AjaxHelper
b) XDocument
c) JsonResult
d) DataContractJsonSerializer
2) شما در حال طراحی یک برنامه ASP.NET MVC هستید. محتوای یک View باید بر اساس نیازمندیهای زیر تشکیل شود:
الف) ارائه محتوای رندر شده user controls/partial views به مرورگر
ب) کار انتخاب user controls/partial views مناسب در اکشن متد کنترلر باید انجام شود
استفاده از کدام روش زیر را توصیه میکنید؟
a) Use the Html.RenderPartial extension method
b) Use the Html.RenderAction extension method
c) Use the PartialViewResult class
d) Use the ContentResult class
3) در حین طراحی یک برنامه ASP.NET MVC، نیاز است منطق مدیریت استثناهای رخ داده و همچنین ثبت وقایع مرتبط را در یک مکان یا کلاس مرکزی مدیریت کنید. کدام روش زیر را پیشنهاد میدهید؟
a) استفاده از try/catch در تمام متدها
b) تحریف متد OnException در کنترلرها
c) مزین سازی تمام کنترلرها به ویژگی HandleError سفارشی شده
d) مزین سازی تمام کنترلرها به ویژگی HandleError پیش فرض
4) شما در حال توزیع برنامهی ASP.NET MVC خود جهت اجرا بر روی IIS 6.x هستید. چه ملاحظاتی را باید مدنظر داشته باشید تا برنامه به درستی کار کند؟
a) تنظیم IIS به نحویکه تمام درخواستها را بر اساس wildcard خاصی به aspnet_isapi.dll هدایت کند.
b) تنظیم IIS به نحویکه تمام درخواستها را بر اساس wildcard خاصی به aspnet_wp.exe هدایت کند.
c) تغییر برنامه به نحویکه تمام درخواستها را به یک HttpHandler خاص هدایت کند.
d) تغییر برنامه به نحویکه تمام درخواستها را به یک HttpModule خاص هدایت کند.
5) شما در حال توسعه برنامهی ASP.NET MVC هستید که در پوشه Views/Shared/DisplayTemplates آن، فایلی به نام score.cshtml به عنوان یک templated helper نمایش سفارشی اعداد صحیح تعریف شده است. مدل برنامه هم مطابق تعاریف زیر است:
public class Player
{
public String Name { get; set; }
public int LastScore { get; set; }
public int HighScore { get; set; }
}
در اینجا اگر نیاز باشد تا فایل score.cshtml یاد شده به صورت خودکار به خاصیت LastScore در حین فراخوانی متد HtmlHelper.DisplayForModel اعمال شود، چه روشی را پیشنهاد میدهید؟
a) فایل score.cshtml باید به LastScore.cshtml تغییر نام یابد.
b) فایل یاد شده باید از پوشه Views/Shared/DisplayTemplates به پوشه Views/Player/DisplayTemplates منتقل شود.
c) باید از ویژگی UIHint به همراه مقدار score جهت مزین سازی خاصیت LastScore استفاده کرد.
[UIHint("Score")]
[Display(Name="LastScore", ShortName="Score")]
6) شما در حال طراحی برنامهی ASP.NET MVC هستید که در آن متد Edit کنترلری باید تنها توسط کاربران اعتبارسنجی شده قابل دسترسی باشد. استفاده از کدام دو گزینه زیر را برای این منظور توصیه میکنید؟
a) [Authorize(Users = "")]
b) [Authorize(Roles = "")]
c) [Authorize(Users = "*")]
d) [Authorize(Roles = "*")]
7) قطعه کد HTML زیر را درنظر بگیرید:
<span id="ref">
<a name=Reference>Check out</a>
the FAQ on
<a href="http://www.contoso.com">
Contoso</a>'s web site for more information:
<a href="http://www.contoso.com/faq">FAQ</a>.
</span>
<a href="http://www.contoso.com/home">Home</a>
قصد داریم به کمک jQuery در span ایی با id مساوی ref، متن تمام لینکها را ضخیم کنیم. کدام گزینه زیر را پیشنهاد میدهید؟
a) $("#ref").filter("a[href]").bold();
b) $("ref").filter("a").css("bold");
c) $("a").css({fontWeight:"bold"});
d) $("#ref a[href]").css({fontWeight:"bold"});
ارتباطات بلادرنگ و SignalR
- من از جزئیات کار شما اطلاعی ندارم. نه خطایی را عنوان کردید و نه پروژهای برای دیباگ پیوست شده. ایشان عنوان کرده که اجرا میشود؛ یک فیلم هم پیوست کرده. ضمنا با jQuery Ajax کار کرده قسمتی را. یعنی یک سری پیشنیاز دیگر را هم باید به پروژه و صفحه اضافه کنید. در کل از راه دور و بدون دیدن کار شما نمیشود نظر داد (کل کار البته).
- در آن مقاله سایت ثالث، dependency_OnChange فقط زمانی رجیستر میشود که GetData یکبار فراخوانی شود. ضمنا این کد نشتی حافظه دارد. چون مدام دارد new OnChangeEventHandler را ایجاد میکند بدون اینکه فکری برای حذف موارد ثبت شده کند. همچنین JobInfoRepository را در سطح یک Web API Controller وهله سازی کرده. یعنی این وهله به ازای هر درخواست رسیده یکبار ایجاد میشود (ونه اینکه یکبار ایجاد شده و بارها استفاده شود) و به این ترتیب یکبار دیگر نیز OnChangeEventHandler رجیستر خواهد شد. خلاصه اینکه روش مناسبی نبوده و توصیه نمیشود.
مطلبت حذف شد. تکرار کنی خودت هم حذف میشی. شک نداشته باش.
- ضمنا نیازی نیست اطلاعات select را در سمت سرور تولید کنید. امکان دریافت JSON از سرور و تبدیل آن به فرمت مورد نظر در سمت کلاینت هم پیش بینی شدهاست:
editoptions: { dataUrl: '...url to get json....', buildSelect: function (response) { var data = typeof response === "string" ? $.parseJSON(response.responseText) : response, var s = "<select>"; s += '<option value="0">--No Manager--</option>'; $.each(data, function () { s += '<option value="' + this.EmployeeId + '">' + this.EmployeeName + '</option>'; }); return s + "</select>"; } }
- چطور یک اپلیکیشن وب ASP.NET MVC 5 بسازید و آن را روی یک وب سایت Windows Azure منتشر کنید.
- چگونه از OAuth، OpenID و سیستم عضویت ASP.NET برای ایمن سازی اپلیکیشن خود استفاده کنید.
- چگونه از API جدید سیستم عضویت برای مدیریت اعضا و نقشها استفاده کنید.
- چگونه از یک دیتابیس SQL برای ذخیره دادهها در Windows Azure استفاده کنید.
توجه: برای تمام کردن این مقاله به یک حساب کاربری Windows Azure نیاز دارید، که بصورت رایگان میتوانید آن را بسازید. برای اطلاعات بیشتر به Windows Azure Free Trial مراجعه کنید.
در این مقاله:
- برپایی محیط توسعه (development environment)
- برپایی محیط Windows Azure
- ایجاد یک اپلیکیشن ASP.NET MVC 5
- توزیع اپلیکیشن روی Windows Azure
- افزودن یک دیتابیس به اپلیکیشن
- افزودن یک OAuth Provider
- استفاده از Membership API
- توزیع اپلیکیشن روی Windows Azure
- قدمهای بعدی
برپایی محیط توسعه
هنگامی که این مرحله با موفقیت به اتمام رسید، تمام ابزار لازم برای شروع به کار را در اختیار دارید.
برپایی محیط Windows Azure
وب سایت Windows Azure شما در یک محیط اشتراکی (shared) میزبانی میشود، و این بدین معنا است که وب سایتهای شما روی ماشینهای مجازی (virtual machines) اجرا میشوند که با مشتریان دیگر Windows Azure به اشتراک گذاشته شده اند. یک محیط میزبانی اشتراکی گزینه ای کم هزینه برای شروع کار با رایانشهای ابری است. اگر در آینده ترافیک وب سایت شما رشد چشم گیری داشته باشد، میتوانید اپلیکیشن خود را طوری توسعه دهید که به نیازهای جدید پاسخگو باشد و آن را روی یک ماشین مجازی اختصاصی (dedicated VMs) میزبانی کنید. اگر معماری پیچیدهتری نیاز دارید، میتوانید به یک سرویس Windows Azure Cloud مهاجرت کنید. سرویسهای ابری روی ماشینهای مجازی اختصاصی اجرا میشوند که شما میتوانید تنظیمات آنها را بر اساس نیازهای خود پیکربندی کنید.
- در پرتال مدیریتی Windows Azure روی Web Sites در قسمت چپ صفحه کلیک کنید، و گزینه New را برگزینید.
- روی Web Site و سپس Custom Create کلیک کنید.
- در مرحله Create Web Site در قسمت URL یک رشته وارد کنید که آدرسی منحصر بفرد برای اپلیکیشن شما خواهد بود. آدرس کامل وب سایت شما، ترکیبی از مقدار این فیلد و مقدار روبروی آن است.
- در لیست Database گزینه Create a free 20 MB SQL Database را انتخاب کنید.
- در لیست Region همان مقداری را انتخاب کنید که برای وب سایت تان انتخاب کرده اید. تنظیمات این قسمت مشخص میکند که ماشین مجازی (VM) شما در کدام مرکز داده (data center) خواهد بود.
- در قسمت DB Connection String Name مقدار پیش فرض DefaultConnection را بپذیرید.
- دکمه فلش پایین صفحه را کلیک کنید تا به مرحله بعد، یعنی مرحله Specify Database Settings بروید.
- در قسمت Name مقدار ContactDB را وارد کنید (تصویر زیر).
- در قسمت Server گزینه New SQL Database Server را انتخاب کنید. اگر قبلا دیتابیس ساخته اید میتوانید آن را از کنترل dropdown انتخاب کنید.
- مقدار قسمت Region را به همان مقداری که برای ایجاد وب سایت تان تنظیم کرده اید تغییر دهید.
- یک Login Name و Password مدیر (administrator) وارد کنید. اگر گزینه New SQL Database server را انتخاب کرده اید، چنین کاربری وجود ندارد و در واقع اطلاعات یک حساب کاربری جدید را وارد میکنید تا بعدا هنگام دسترسی به دیتابیس از آن استفاده کنید. اگر دیتابیس دیگری را از لیست انتخاب کرده باشید، اطلاعات یک حساب کاربری موجود از شما دریافت خواهد شد. در مثال این مقاله ما گزینه Advanced را رها میکنیم. همچنین در نظر داشته باشید که برای دیتابیسهای رایگان تنها از یک Collation میتوانید استفاده کنید.
دکمه تایید پایین صفحه را کلیک کنید تا مراحل تمام شود.
تصویر زیر استفاده از یک SQL Server و حساب کاربری موجود (existing) را نشان میدهد.
پرتال مدیریتی پس از اتمام مراحل، به صفحه وب سایتها باز میگردد. ستون Status نشان میدهد که سایت شما در حال ساخته شدن است. پس از مدتی (معمولا کمتر از یک دقیقه) این ستون نشان میدهد که سایت شما با موفقیت ایجاد شده. در منوی پیمایش سمت چپ، تعداد سایت هایی که در اکانت خود دارید در کنار آیکون Web Sites نمایش داده شده است، تعداد دیتابیسها نیز در کنار آیکون SQL Databases نمایش داده میشود.
یک اپلیکیشن ASP.NET MVC 5 بسازید
نوع پروژه را ASP.NET Web Application انتخاب کنید.
نکته: در تصویر بالا نام پروژه "MyExample" است اما حتما نام پروژه خود را به "ContactManager" تغییر دهید. قطعه کدهایی که در ادامه مقاله خواهید دید نام پروژه را ContactManager فرض میکنند.
در دیالوگ جدید ASP.NET نوع اپلیکیشن را MVC انتخاب کنید و دکمه Change Authentication را کلیک کنید.
گزینه پیش فرض Individual User Accounts را بپذیرید. برای اطلاعات بیشتر درباره متدهای دیگر احراز هویت به این لینک مراجعه کنید. دکمههای OK را کلیک کنید تا تمام مراحل تمام شوند.
تنظیم تیتر و پاورقی سایت
- فایل Layout.cshtml_ را باز کنید. دو نمونه از متن "My ASP.NET MVC Application" را با عبارت "Contact Manager" جایگزین کنید.
- عبارت "Application name" را هم با "CM Demo" جایگزین کنید.
اپلیکیشن را بصورت محلی اجرا کنید
اپلیکیشن شما فعلا آماده است و میتوانید آن را روی Windows Azure توزیع کنید. بعدا دیتابیس و دسترسی داده نیز اضافه خواهد شد.
اپلیکیشن را روی Windows Azure منتشر کنید
حال دیالوگ Import Publish Profile نمایش داده میشود.
یکی از متدهای زیر را استفاده کنید تا ویژوال استودیو بتواند به اکانت Windows Azure شما متصل شود.
- روی Sign In کلیک کنید تا با وارد کردن اطلاعات حساب کاربری وارد Windows Azure شوید.
- روی Manage subscriptions کلیک کنید تا یک management certificate نصب کنید، که دسترسی به حساب کاربری شما را ممکن میسازد.
در دیالوگ باکس Publish Web روی Publish کلیک کنید.
اپلیکیشن شما حالا در فضای ابری اجرا میشود. دفعه بعد که اپلیکیشن را منتشر کنید تنها فایلهای تغییر کرده (یا جدید) آپلود خواهند شد.
یک دیتابیس به اپلیکیشن اضافه کنید
کلاسهای مدل Contacts را اضافه کنید
نام کلاس را به Contact.cs تغییر دهید و دکمه Add را کلیک کنید.
کد فایل Contact.cs را با قطعه کد زیر مطابقت دهید.
using System.ComponentModel.DataAnnotations; using System.Globalization; namespace ContactManager.Models { public class Contact { public int ContactId { get; set; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } [DataType(DataType.EmailAddress)] public string Email { get; set; } } }
این کلاس موجودیت Contact را در دیتابیس معرفی میکند. داده هایی که میخواهیم برای هر رکورد ذخیره کنیم تعریف شده اند، بعلاوه یک فیلد Primary Key که دیتابیس به آن نیاز دارد.
یک کنترلر و نما برای دادهها اضافه کنید
در دیالوگ باکس Add Scaffold گزینه MVC 5 Controller with views, using EF را انتخاب کنید.
در دیالوگ Add Controller نام "CmController" را برای کنترلر وارد کنید. (تصویر زیر.)
در لیست Model گزینه (Contact (ContactManager.Models را انتخاب کنید.
در قسمت Data context class گزینه (ApplicationDbContext (ContactManager.Models را انتخاب کنید. این ApplicationDbContext هم برای اطلاعات سیستم عضویت و هم برای دادههای Contacts استفاده خواهد شد.
روی Add کلیک کنید. ویژوال استودیو بصورت خودکار با استفاده از Scaffolding متدها و Viewهای لازم برای عملیات CRUD را فراهم میکند، که همگی از مدل Contact استفاده میکنند.
فعالسازی مهاجرت ها، ایجاد دیتابیس، افزودن داده نمونه و یک راه انداز
در پنجره باز شده فرمان زیر را وارد کنید.
enable-migrations
فرمان enable-migrations یک پوشه با نام Migrations می سازد و فایلی با نام Configuration.cs را به آن اضافه میکند. با استفاده از این کلاس میتوانید دادههای اولیه دیتابیس را وارد کنید و مهاجرتها را نیز پیکربندی کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
add-migration Initial
فرمان add-migration initial فایلی با نام data_stamp> initial> ساخته و آن را در پوشه Migrations ذخیره میکند. در این مرحله دیتابیس شما ایجاد میشود. در این فرمان، مقدار initial اختیاری است و صرفا برای نامگذاری فایل مهاجرت استفاده شده. فایلهای جدید را میتوانید در Solution Explorer مشاهده کنید.
در کلاس Initial متد Up جدول Contacts را میسازد. و متد Down (هنگامی که میخواهید به وضعیت قبلی بازگردید) آن را drop میکند.
حال فایل Migrations/Configuration.cs را باز کنید. فضای نام زیر را اضافه کنید.
using ContactManager.Models;
حال متد Seed را با قطعه کد زیر جایگزین کنید.
protected override void Seed(ContactManager.Models.ApplicationDbContext context) { context.Contacts.AddOrUpdate(p => p.Name, new Contact { Name = "Debra Garcia", Address = "1234 Main St", City = "Redmond", State = "WA", Zip = "10999", Email = "debra@example.com", }, new Contact { Name = "Thorsten Weinrich", Address = "5678 1st Ave W", City = "Redmond", State = "WA", Zip = "10999", Email = "thorsten@example.com", }, new Contact { Name = "Yuhong Li", Address = "9012 State st", City = "Redmond", State = "WA", Zip = "10999", Email = "yuhong@example.com", }, new Contact { Name = "Jon Orton", Address = "3456 Maple St", City = "Redmond", State = "WA", Zip = "10999", Email = "jon@example.com", }, new Contact { Name = "Diliana Alexieva-Bosseva", Address = "7890 2nd Ave E", City = "Redmond", State = "WA", Zip = "10999", Email = "diliana@example.com", } ); }
این متد دیتابیس را Seed میکند، یعنی دادههای پیش فرض و اولیه دیتابیس را تعریف میکند. برای اطلاعات بیشتر به Seeding and Debugging Entity Framework (EF) DBs مراجعه کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
update-database
فرمان update-database مهاجرت نخست را اجرا میکند، که دیتابیس را میسازد. بصورت پیش فرض این یک دیتابیس SQL Server Express LocalDB است.
حال پروژه را با CTRL + F5 اجرا کنید.
همانطور که مشاهده میکنید، اپلیکیشن دادههای اولیه (Seed) را نمایش میدهد، و لینک هایی هم برای ویرایش، حذف و مشاهده جزئیات رکوردها فراهم میکند. میتوانید دادهها را مشاهده کنید، رکورد جدید ثبت کنید و یا دادههای قبلی را ویرایش و حذف کنید.
یک تامین کننده OAuth2 و OpenID اضافه کنید
استفاده از Membership API
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework;
bool AddUserAndRole(ContactManager.Models.ApplicationDbContext context) { IdentityResult ir; var rm = new RoleManager<IdentityRole> (new RoleStore<IdentityRole>(context)); ir = rm.Create(new IdentityRole("canEdit")); var um = new UserManager<ApplicationUser>( new UserStore<ApplicationUser>(context)); var user = new ApplicationUser() { UserName = "user1", }; ir = um.Create(user, "Passw0rd1"); if (ir.Succeeded == false) return ir.Succeeded; ir = um.AddToRole(user.Id, "canEdit"); return ir.Succeeded; }
protected override void Seed(ContactManager.Models.ApplicationDbContext context) { AddUserAndRole(context); context.Contacts.AddOrUpdate(p => p.Name, // Code removed for brevity }
کدی موقتی برای تخصیص نقش canEdit به کاربران جدید Social Provider ها
await UserManager.AddToRoleAsync(user.Id, "CanEdit");
در ادامه مقاله اپلیکیشن خود را روی Windows Azure منتشر خواهید کرد و با استفاده از Google و تامین کنندگان دیگر وارد سایت میشوید. هر فردی که به آدرس سایت شما دسترسی داشته باشد، و یک حساب کاربری Google هم در اختیار داشته باشد میتواند در سایت شما ثبت نام کند و سپس دیتابیس را ویرایش کند. برای جلوگیری از دسترسی دیگران، میتوانید وب سایت خود را متوقف (stop) کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
Update-Database
فرمان را اجرا کنید تا متد Seed را فراخوانی کند. حال AddUserAndRole شما نیز اجرا میشود. تا این مرحله نقش canEdit ساخته شده و کاربر جدیدی با نام user1 ایجاد و به آن افزوده شده است.
محافظت از اپلیکیشن توسط SSL و خاصیت Authorize
در این قسمت شما با استفاده از خاصیت Authorize دسترسی به اکشن متدها را محدود میکنید. کاربران ناشناس (Anonymous) تنها قادر به مشاهده متد Index در کنترلر home خواهند بود. کاربرانی که ثبت نام کرده اند به متدهای Index و Details در کنترلر Cm و صفحات About و Contact نیز دسترسی خواهند داشت. همچنین دسترسی به متدهایی که دادهها را تغییر میدهند تنها برای کاربرانی وجود دارد که در نقش canEdit هستند.
خاصیت Authorize و RequireHttps را به اپلیکیشن اضافه کنید. یک راه دیگر افزودن این خاصیتها به تمام کنترلرها است، اما تجارب امنیتی توصیه میکند که این خاصیتها روی کل اپلیکیشن اعمال شوند. با افزودن این خاصیتها بصورت global تمام کنترلرها و اکشن متدهایی که میسازید بصورت خودکار محافظت خواهند شد، و دیگر لازم نیست بیاد داشته باشید کدام کنترلرها و متدها را باید ایمن کنید.
برای اطلاعات بیشتر به Securing your ASP.NET MVC App and the new AllowAnonymous Attribute مراجعه کنید.
فایل App_Start/FilterConfig.cs را باز کنید و متد RegisterGlobalFilters را با کد زیر مطابقت دهید.
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new System.Web.Mvc.AuthorizeAttribute()); filters.Add(new RequireHttpsAttribute()); }
خاصیت Authorize در کد بالا از دسترسی کاربران ناشناس به تمام متدهای اپلیکیشن جلوگیری میکند. شما برای اعطای دسترسی به متدهایی خاص از خاصیت AllowAnonymous استفاده خواهید کرد. در آخر خاصیت RequireHTTPS باعث میشود تا تمام دسترسیها به اپلیکیشن وب شما از طریق HTTPS صورت گیرد.
حالا خاصیت AllowAnonymous را به متد Index در کنترلر Home اضافه کنید. از این خاصیت برای اعطای دسترسی به تمامی کاربران سایت استفاده کنید. قسمتی از کد کنترلر Home را در زیر میبینید.
namespace ContactManager.Controllers { public class HomeController : Controller { [AllowAnonymous] public ActionResult Index() { return View(); }
یک جستجوی عمومی برای عبارت AllowAnonymous انجام دهید. همانطور که مشاهده میکنید این خاصیت توسط متدهای ورود و ثبت نام در کنترلر Account نیز استفاده شده است.
در کنترلر CmController خاصیت [("Authorize(Roles="canEdit] را به تمام متدهایی که با داده سر و کار دارند اضافه کنید، به غیر از متدهای Index و Details. قسمتی از کد کامل شده در زیر آمده است.
فعال سازی SSL برای پروژه
روی نام پروژه کلیک راست کنید و Properties را انتخاب کنید. در قسمت چپ گزینه Web را انتخاب کنید. حالا مقدار Project Url را به آدرسی که کپی کرده اید تغییر دهید. نهایتا تغییرات را ذخیره کنید و پنجره را ببندید.
حال پروژه را اجرا کنید. مرورگر شما باید یک پیام خطای اعتبارسنجی به شما بدهد. دلیلش این است که اپلیکیشن شما از یک Valid Certificate استفاده نمیکند. هنگامی که پروژه را روی Windows Azure منتشر کنید دیگر این پیغام را نخواهید دید. چرا که سرورهای مایکروسافت همگی لایسنسهای معتبری دارند. برای اپلیکیشن ما میتوانید روی Continue to this website را انتخاب کنید.
حال مرورگر پیش فرض شما باید صفحه Index از کنترلر home را به شما نمایش دهد.
اگر از یک نشست قبلی هنوز در سایت هستید (logged-in) روی لینک Log out کلیک کنید و از سایت خارج شوید.
روی لینکهای About و Contact کلیک کنید. باید به صفحه ورود به سایت هدایت شوید چرا که کاربران ناشناس اجازه دسترسی به این صفحات را ندارند.
روی لینک Register کلیک کنید و یک کاربر محلی با نام Joe بسازید. حال مطمئن شوید که این کاربر به صفحات Home, About و Contact دسترسی دارد.
روی لینک CM Demo کلیک کنید و مطمئن شوید که دادهها را مشاهده میکنید.
حال روی یکی از لینکهای ویرایش (Edit) کلیک کنید. این درخواست باید شما را به صفحه ورود به سایت هدایت کند، چرا که کاربران محلی جدید به نقش canEdit تعلق ندارند.
با کاربر user1 که قبلا ساختید وارد سایت شوید. حال به صفحه ویرایشی که قبلا درخواست کرده بودید هدایت میشوید.
اگر نتوانستید با این کاربر به سایت وارد شوید، کلمه عبور را از سورس کد کپی کنید و مجددا امتحان کنید. اگر همچنان نتوانستید به سایت وارد شوید، جدول AspNetUsers را بررسی کنید تا مطمئن شوید کاربر user1 ساخته شده است. این مراحل را در ادامه مقاله خواهید دید.
در آخر اطمینان حاصل کنید که میتوانید دادهها را تغییر دهید.
اپلیکیشن را روی Windows Azure منتشر کنید
در دیالوگ باز شده روی قسمت Settings کلیک کنید. روی File Publish Options کلیک کنید تا بتوانید Remote connection string را برای ApplicationDbContext و دیتابیس ContactDB انتخاب کنید.
اگر ویژوال استودیو را پس از ساخت Publish profile بسته و دوباره باز کرده اید، ممکن است رشته اتصال را در لیست موجود نبینید. در چنین صورتی، بجای ویرایش پروفایل انتشار، یک پروفایل جدید بسازید. درست مانند مراحلی که پیشتر دنبال کردید.
زیر قسمت ContactManagerContext گزینه Execute Code First Migrations را انتخاب کنید.
حال Publish را کلیک کنید تا اپلیکیشن شما منتشر شود. با کاربر user1 وارد سایت شوید و بررسی کنید که میتوانید دادهها را ویرایش کنید یا خیر.
حال از سایت خارج شوید و توسط یک اکانت Google یا Facebook وارد سایت شوید، که در این صورت نقش canEdit نیز به شما تعلق میگیرد.
برای جلوگیری از دسترسی دیگران، وب سایت را متوقف کنید
یک راه دیگر متوقف کردن وب سایت از طریق پرتال مدیریت Windows Azure است.
فراخوانی AddToRoleAsync را حذف و اپلیکیشن را منتشر و تست کنید
await UserManager.AddToRoleAsync(user.Id, "CanEdit");
دکمه Start Preview را فشار دهید. در این مرحله تنها فایل هایی که نیاز به بروز رسانی دارند آپلود خواهند شد.
وب سایت را راه اندازی کنید. سادهترین راه از طریق پرتال مدیریت Windows Azure است. توجه داشته باشید که تا هنگامی که وب سایت شما متوقف شده، نمیتوانید اپلیکیشن خود را منتشر کنید.
حال به ویژوال استودیو بازگردید و اپلیکیشن را منتشر کنید. اپلیکیشن Windows Azure شما باید در مرورگر پیش فرض تان باز شود. حال شما در حال مشاهده صفحه اصلی سایت بعنوان یک کاربر ناشناس هستید.
روی لینک About کلیک کنید، که شما را به صفحه ورود هدایت میکند.
روی لینک Register در صفحه ورود کلیک کنید و یک حساب کاربری محلی بسازید. از این حساب کاربری برای این استفاده میکنیم که ببینیم شما به صفحات فقط خواندنی (read-only) و نه صفحاتی که دادهها را تغییر میدهند دسترسی دارید یا خیر. بعدا در ادامه مقاله، دسترسی حسابهای کاربری محلی (local) را حذف میکنیم.
مطمئن شوید که به صفحات About و Contact دسترسی دارید.
لینک CM Demo را کلیک کنید تا به کنترلر CmController هدایت شوید.
روی یکی از لینکهای Edit کلیک کنید. این کار شما را به صفحه ورود به سایت هدایت میکند. در زیر قسمت User another service to log in یکی از گزینههای Google یا Facebook را انتخاب کنید و توسط حساب کاربری ای که قبلا ساختید وارد شوید.
حال بررسی کنید که امکان ویرایش اطلاعات را دارید یا خیر.
نکته: شما نمیتوانید در این اپلیکیشن از اکانت گوگل خود خارج شده، و با همان مرورگر با اکانت گوگل دیگری وارد اپلیکیشن شوید. اگر دارید از یک مرورگر استفاده میکنید، باید به سایت گوگل رفته و از آنجا خارج شوید. برای وارد شدن به اپلیکیشن توسط یک اکانت دیگر میتوانید از یک مرورگر دیگر استفاده کنید.
دیتابیس SQL Azure را بررسی کنید
توجه: اگر نمیتوانید گره SQL Databases را باز کنید و یا ContactDB را در ویژوال استودیو نمیبینید، باید مراحلی را طی کنید تا یک پورت یا یکسری پورت را به فایروال خود اضافه کنید. دقت داشته باشید که در صورت اضافه کردن Port Rangeها ممکن است چند دقیقه زمان نیاز باشد تا بتوانید به دیتابیس دسترسی پیدا کنید.
روی جدول AspNetUsers کلیک راست کرده و View Data را انتخاب کنید.
حالا روی AspNetUserRoles کلیک راست کنید و View Data را انتخاب کنید.
اگر شناسه کاربران (User ID) را بررسی کنید، مشاهده میکنید که تنها دو کاربر user1 و اکانت گوگل شما به نقش canEdit تعلق دارند.
Cannot open server login error
شما باید آدرس IP خود را به لیست آدرسهای مجاز (Allowed IPs) اضافه کنید. در پرتال مدیریتی Windows Azure در قسمت چپ صفحه، گزینه SQL Databases را انتخاب کنید.
دیتابیس مورد نظر را انتخاب کنید. حالا روی لینک Set up Windows Azure firewall rules for this IP address کلیک کنید.
هنگامی که با پیغام "?The current IP address xxx.xxx.xxx.xxx is not included in existing firewall rules. Do you want to update the firewall rules" مواجه شدید Yes را کلیک کنید. افزودن یک آدرس IP بدین روش معمولا کافی نیست و در فایروالهای سازمانی و بزرگ باید Range بیشتری را تعریف کنید.
مرحله بعد اضافه کردن محدوده آدرسهای مجاز است.
مجددا در پرتال مدیریتی Windows Azure روی SQL Databases کلیک کنید. سروری که دیتابیس شما را میزبانی میکند انتخاب کنید.
در بالای صفحه لینک Configure را کلیک کنید. حالا نام rule جدید، آدرس شروع و پایان را وارد کنید.
در پایین صفحه Save را کلیک کنید.
در آخر میتوانید توسط SSOX به دیتابیس خود متصل شوید. از منوی View گزینه SQL Server Object Explorer را انتخاب کنید. روی SQL Server کلیک راست کرده و Add SQL Server را انتخاب کنید.
در دیالوگ Connect to Server متد احراز هویت را به SQL Server Authentication تغییر دهید. این کار نام سرور و اطلاعات ورود پرتال Windows Azure را به شما میدهد.
در مرورگر خود به پرتال مدیریتی بروید و SQL Databases را انتخاب کنید. دیتابیس ContactDB را انتخاب کرده و روی View SQL Database connection strings کلیک کنید. در صفحه Connection Strings مقادیر Server و User ID را کپی کنید. حالا مقادیر را در دیالوگ مذکور در ویژوال استودیو بچسبانید. مقدار فیلد User ID در قسمت Login وارد میشود. در آخر هم کلمه عبوری که هنگام ساختن دیتابیس تنظیم کردید را وارد کنید.
حالا میتوانید با مراحلی که پیشتر توضیح داده شد به دیتابیس Contact DB مراجعه کنید.
افزودن کاربران به نقش canEdit با ویرایش جداول دیتابیس
حالا RoleId را کپی کنید و در ردیف جدید بچسبانید.
شناسه کاربر مورد نظر را از جدول AspNetUsers پیدا کنید و مقدار آن را در ردیف جدید کپی کنید. همین! کاربر جدید شما به نقش canEdit اضافه شد.
نکاتی درباره ثبت نام محلی (Local Registration)
- در کنترلر Account متدهای Register را ویرایش کنید و خاصیت AllowAnonymous را از آنها حذف کنید (هر دو متد GET و POST). این کار ثبت نام کاربران ناشناس و بدافزارها (bots) را غیر ممکن میکند.
- در پوشه Views/Shared فایل LoginPartial.cshtml_ را باز کنید و لینک Register را از آن حذف کنید.
- در فایل Views/Account/Login.cshtml نیز لینک Register را حذف کنید.
- اپلیکیشن را دوباره منتشر کنید.
قدمهای بعدی
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductDetail >ng g c product/ProductEdit
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component'; import { ProductDetailComponent } from './product-detail/product-detail.component'; import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent }, { path: 'products/:id/edit', component: ProductEditComponent } ];
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
> ng g i product/IProduct
export interface IProduct { id: number; productName: string; productCode: string; }
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
>npm install angular-in-memory-web-api --save
"dependencies": { "angular-in-memory-web-api": "^0.3.1" },
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
> ng g cl product/ProductData
import { IProduct } from './iproduct'; import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api'; export class ProductData implements InMemoryDbService, InMemoryBackendConfig { createDb() { let products: IProduct[] = [ { 'id': 1, 'productName': 'Product 1', 'productCode': '0011' }, { 'id': 2, 'productName': 'Product 2', 'productCode': '0023' }, { 'id': 5, 'productName': 'Product 5', 'productCode': '0048' }, { 'id': 8, 'productName': 'Product 8', 'productCode': '0022' }, { 'id': 10, 'productName': 'Product 10', 'productCode': '0042' } ]; return { products }; } }
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, AppRoutingModule ],
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
>ng g s product/product -m product/product.module
installing service create src\app\product\product.service.spec.ts create src\app\product\product.service.ts update src\app\product\product.module.ts
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './iproduct'; @Injectable() export class ProductService { private baseUrl = 'api/products'; constructor(private http: Http) { } getProducts(): Observable<IProduct[]> { return this.http.get(this.baseUrl) .map(this.extractData) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.handleError); } getProduct(id: number): Observable<IProduct> { if (id === 0) { return Observable.of(this.initializeProduct()); }; const url = `${this.baseUrl}/${id}`; return this.http.get(url) .map(this.extractData) .do(data => console.log('getProduct: ' + JSON.stringify(data))) .catch(this.handleError); } deleteProduct(id: number): Observable<Response> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); const url = `${this.baseUrl}/${id}`; return this.http.delete(url, options) .do(data => console.log('deleteProduct: ' + JSON.stringify(data))) .catch(this.handleError); } saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); } private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { product.id = undefined; return this.http.post(this.baseUrl, product, options) .map(this.extractData) .do(data => console.log('createProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { const url = `${this.baseUrl}/${product.id}`; return this.http.put(url, product, options) .map(() => product) .do(data => console.log('updateProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private extractData(response: Response) { let body = response.json(); return body.data || {}; } private handleError(error: Response): Observable<any> { console.error(error); return Observable.throw(error.json().error || 'Server error'); } initializeProduct(): IProduct { // Return an initialized object return { id: 0, productName: null, productCode: null }; } }
private baseUrl = 'api/products';
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle = 'Product List'; errorMessage: string; products: IProduct[]; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => this.errorMessage = <any>error); } }
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Product</th> <th>Code</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td><a [routerLink]="['/products', product.id]"> {{product.productName}} </a> </td> <td>{{ product.productCode}}</td> <td> <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </td> </tr> </tbody> </table> </div> </div> </div>
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
<a [routerLink]="['/products', product.id]"> {{product.productName}} </a>
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a>
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
<li> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li>
saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); }
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
this.router.navigate(['products', this.product.id]);
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
let id = +this.route.snapshot.params['id'];
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
{ path: 'products/:id', component: ProductDetailComponent }
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } );
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'] }) export class ProductDetailComponent implements OnInit { pageTitle = 'Product Detail'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number) { this.productService.getProduct(id).subscribe( product => this.product = product, error => this.errorMessage = <any>error); } }
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product"> <div class="panel-heading" style="font-size:large"> {{pageTitle + ": " + product.productName}} </div> <div class="panel-body"> <div> Name: {{product.productName}} </div> <div> Code: {{product.productCode}} </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> <div class="panel-footer"> <a class="btn btn-default" [routerLink]="['/products']"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a> <a class="btn btn-primary" style="width:80px;margin-left:10px" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </div> </div>
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, ProductRoutingModule ]
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { pageTitle = 'Product Edit'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number): void { this.productService.getProduct(id) .subscribe( (product: IProduct) => this.onProductRetrieved(product), (error: any) => this.errorMessage = <any>error ); } onProductRetrieved(product: IProduct): void { this.product = product; if (this.product.id === 0) { this.pageTitle = 'Add Product'; } else { this.pageTitle = `Edit Product: ${this.product.productName}`; } } deleteProduct(): void { if (this.product.id === 0) { // Don't delete, it was never saved. this.onSaveComplete(); } else { if (confirm(`Really delete the product: ${this.product.productName}?`)) { this.productService.deleteProduct(this.product.id) .subscribe( () => this.onSaveComplete(`${this.product.productName} was deleted`), (error: any) => this.errorMessage = <any>error ); } } } saveProduct(): void { if (true === true) { this.productService.saveProduct(this.product) .subscribe( () => this.onSaveComplete(`${this.product.productName} was saved`), (error: any) => this.errorMessage = <any>error ); } else { this.errorMessage = 'Please correct the validation errors.'; } } onSaveComplete(message?: string): void { if (message) { // TODO: show msg } // Navigate back to the product list this.router.navigate(['/products']); } }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty) && !productNameVar.valid }"> <label class="col-md-2 control-label" for="productNameId">Product Name</label> <div class="col-md-8"> <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" required minlength="3" [(ngModel)]=product.productName name="productName" #productNameVar="ngModel" /> <span class="help-block" *ngIf="(productNameVar.touched || productNameVar.dirty) && productNameVar.errors"> <span *ngIf="productNameVar.errors.required"> Product name is required. </span> <span *ngIf="productNameVar.errors.minlength"> Product name must be at least three characters. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || productCodeVar.dirty) && !productCodeVar.valid }"> <label class="col-md-2 control-label" for="productCodeId">Product Code</label> <div class="col-md-8"> <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" required [(ngModel)]=product.productCode name="productCode" #productCodeVar="ngModel" /> <span class="help-block" *ngIf="(productCodeVar.touched || productCodeVar.dirty) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!productForm.valid" (click)="saveProduct()"> Save </button> </span> <span> <a class="btn btn-default" [routerLink]="['/products']"> Cancel </a> </span> <span> <a class="btn btn-default" (click)="deleteProduct()"> Delete </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); }
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
ngOnInit(): void { this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } ); }
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
// For Angular 4+ let id = +this.route.snapshot.paramMap.get('id'); this.getProduct(id); // OR this.route.paramMap.subscribe(params => { let id = +params['id']; this.getProduct(id); });
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
{ path: 'products/:name/:code', component: ProductListComponent }
[routerLink]="['/products', productName, productCode]"
{ path: 'products', component: ProductListComponent },
[routerLink]="['/products', {name: productName, code: productCode}]"
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
http://localhost:4200/products;name=Controller;code=gmg
let name = this.route.snapshot.params['name']; let code = this.route.snapshot.params['code'];
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
http://localhost:42000/products/5?filterBy=product1
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
{ path: 'products', component: ProductListComponent}
<a [routerLink] = "['/products', product.id]" [queryParams] = "{ filterBy: 'er', showImage: true }"> {{product.productName}} </a>
و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'], { queryParams: { filterBy: 'er', showImage: true} } );
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
[preserveQueryParams] = "true"
<a class="btn btn-default" [routerLink]="['/products']" [preserveQueryParams]="true"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a>
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
this.router.navigate(['/products'], { queryParamsHandling: 'preserve'} );
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || ''; var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
>ng g p product/ProductFilter
import { PipeTransform, Pipe } from '@angular/core'; import { IProduct } from './iproduct'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], filterBy: string): IProduct[] { filterBy = filterBy ? filterBy.toLocaleLowerCase() : null; return filterBy ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value; } }
<div class="panel-body"> <div class="row"> <div class="col-md-2">Filter by:</div> <div class="col-md-4"> <input type="text" [(ngModel)]="listFilter" /> </div> </div> <div class="row" *ngIf="listFilter"> <div class="col-md-6"> <h3>Filtered by: {{listFilter}} </h3> </div> </div>
<tr *ngFor="let product of products | productFilter:listFilter"> <td><a [routerLink]="['/products', product.id]" [queryParams]="{filterBy: listFilter}"> {{product.productName}} </a> </td>
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { listFilter: string; ngOnInit(): void { this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.