نظرات مطالب
در مطالب قبلی با پروتکل OData و WCF Data Service و قراردادهای کوئری نویسی آن آشنا شدید. حال میخواهیم با استفاده از Jquery به دادههای وب سرویس WCF Data Service دسترسی یابیم. اما پیش نیازهای لازم است
و نمونه آدرس
دومی کتابخانه ای است که مانند روش اول عمل میکند اما به جای ارث برای از کلاس DataService میبایست از کلاس ODataService استفاده نماییم.
با اسفاده از beforeSend مقدار Accept Header و MaxDataServiceVersion را تعیین نموده ایم.
پیش نیاز اول : دسترسی به خروجی Json وب سرویس WCF Data
خروجی پیش فرش وب سرویس WCF Data Services ساختار Xml دارد پس میبایست وب سرویس را متوجه سازیم که ما با خروجی Json نیاز داریم. از نسخه 5 به بعد اگر MaxProtocolVersion را بر روی V3 قرار دهیم دیگر با Accept Header برابر application/json کار نخواهد کرد و میبایست از application/json;odata=verbose استفاده نمود یا نسخه پروتکل را بر روی V2 یا پایینتر تنطیم کنید. علاوه بر آن کتابخانههای و قطعه کدهای تهیه شده است که با پارامتر format$ این کار را برای ما انجام میدهد در زیر آدرس دو نمونه آورده شده است.
قطعه کد اول یک Attribute است که با اضافه کردن آن به بالای کلاس WebService و استفاده از پارامتر format=json$ در آدرس وب سرویس این کار را برای ما انجام میدهد.
[JSONPSupportBehavior] public class Northwind : DataService<NorthwindEntities>
http://localhost:8358/Northwind.svc/Products?$format=json
نکته: در صورتی که بخواهیم از نسخه V3 استفاده نماییم Accept Header را باید به application/json;odata=verbose تغییر دهیم
public class Northwind : ODataService<NorthwindEntities>
استفاده از WCF Data Services به کمک Jquery
تابع getJSON مخصوص درخواستهای است خروجی بصورت json برگردانده میشود اما با نسخه V3 سازگار نمیباشد و از روش پارامتر format$ میتوان استفاده نمود
$.getJSON("Northwind.svc/Products?$format=json", function (data) { $.each(data.d, function (i, item) { $("<p/>").html(item.Product_Name + " " + item.Unit_Price).appendTo("#products"); }); });
همچنین از تابع Ajax که امکاتات بیشتری را در اختیارمان قرار میدهد به راحتی میتوان استفاده نمود به مثال زیر دقت کنید:
$.ajax('Northwind.svc/Products', { dataType: "json", beforeSend: function (xhr) { xhr.setRequestHeader("Accept", "application/json;odata=verbose"); xhr.setRequestHeader("MaxDataServiceVersion", "3.0"); }, success: function (data) { $.each(data.d, function (i, item) { $("<p/>").html(item.Product_Name + " " + item.Unit_Price).appendTo("#products"); }); } });
بنابراین به کمک قراردادههای کوئری نویسی که در مطالب قبلی گفته شد میتوان با استفاده از Url تابع Ajax به داده مورد نظر خود رسید.
مطالب
AngularJS #3
در این مقاله مفاهیم انقیاد داده (Data Binding)، تزریق وابستگی (Dependency Injection)،هدایت گرها (Directives) و سرویسها را بررسی خواهیم کرد و از مقالهی آینده، به بررسی ویژگیها و امکانات AngularJS در قالب مثال خواهیم پرداخت.
انقیاد داده (Data Binding)
سناریو هایی وجود دارد که در آنها باید اطلاعات قسمتی از صفحه به صورت نامتقارن (Asynchronous) با دادههای دریافتی جدید به روز رسانی شود. روش معمول برای انجام چنین کاری؛ دریافت دادهها از سرور است که عموما به فرم HTML میباشند و جایگزینی آن با بخشی از صفحه که قرار است به روز رسانی شود، اما حالتی را در نظر بگیرید که با داده هایی از جنس JSON طرف هستید و اطلاعات صفحه را با این دادهها باید به روز رسانی کنید. معمولا برای حل چنین مشکلی مجبور به نوشتن مقدار زیادی کد هستید تا بتوانید به خوبی اطلاعات View را به روز رسانی کنید. حتما با خودتان فکر کرده اید که قطعا راهی وجود دارد تا بدون نوشتن کدی، قسمتی از View را به Model متناظر خود نگاشت کرده و این دو به صورت بلادرنگ از تغییرات یکدیگر آگاه شوند. این عمل عموما به مفهوم انقیاد داده شناخته میشود و Angular هم به خوبی از انقیاد داده دوطرفه پشتیبانی میکند.
برای مشاهده این ویژگی در Angular، مثال مقالهی قبل را به کدهای زیر تغییر دهید تا پیغام به صورت پویا توسط کاربر وارد شود:
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> </div> <script src="../Scripts/angular.js"></script> </body> </html>
بدون نیاز به حتی یک خط کد نویسی! با مشخص کردن input به عنوان Model از طریق ng-model، خاصیت greeting.text که در داخل {{ }} مشخص شده را به متن داخل textbox مقید (bind) کردیم. نتیجه میگیریم که جفت آکلود {{ }} برای اعمال Data Binding استفاده میشود.
حال یک دکمه نیز بر روی فرم قرار میدهیم که با کلیک کردن بر روی آن، متن داخل textbox را نمایش دهد.
<!DOCTYPE html> <html ng-app> <head> <title>Sample2</title> </head> <body> <div ng-controller="GreetingController"> <input type="text" ng-model="greeting.text" /> <p>{{greeting.text}}, World!</p> <button ng-click="showData()">Show</button> </div> <script src="../Scripts/angular.js"></script> <script> var GreetingController = function ($scope, $window) { $scope.greeting = { text: "Hello" }; $scope.showData = function () { $window.alert($scope.greeting.text); }; }; </script> </body> </html>
به کمک ng-click، تابع showData به هنگام کلیک شدن، فراخوانی میشود. window$ نیز به عنوان پارامتر کلاس GreetingController مشخص شده است. window$ نیز یکی از سرویسهای پیش فرض تعریف شده توسط Angular است و ما در اینجا در سازندهی کلاس آن را به عنوان وابستگی درخواست کرده ایم تا توسط سیستم تزریق وابستگی توکار، نمونهی مناسب آن در اختیار ما بگذارد. window$ نیز تقریبا معادل شی window است و یکی از دلایل استفاده از آن سادهتر شدن نوشتن آزمونهای واحداست.
حال متنی را داخل textbox نوشته و دکمهی show را فشار دهید. متن نوشته شده را به صورت یک popup مشاهده خواهید کرد.
همچنین شی scope$ نیز نمونهی مناسب آن توسط سیستم تزریق وابستگی Angular، در اختیار Controller قرار میگیرد و نمونهی در اختیار قرارگرفته، برای ارتباط با View Model و سیستم انقیاد داده استفاده میشود.
معمولا انقیاد داده در الگوی طراحی (ModelView-ViewModel(MVVM مطرح است و به این دلیل که این الگوی طراحی به خوبی با الگوی طراحی MVC سازگار است، این امکان در Angular گنجانده شده است.
تزریق وابستگی (Dependency Injection)
تا به این جای کار قطعن بارها و بارها اسم آن را خوانده اید. در مثال فوق، پارامتری با نام scope$ را برای سازندهی کنترلر خود در نظر گرفتیم و ما بدون انجام هیچ کاری نمونهی مناسب آن را که برای انجام اعمال انقیاد داده با viewmodel استفاده میشود را دریافت کردیم. به عنوان مثال، window$ را نیز در سازندهی کلاس کنترلر خود به عنوان یک وابستگی تعریف کردیم و تزریق نمونهی مناسب آن توسط سیستم تزریق وابستگی توکار Angular صورت میگرفت.
اگر با IOC Containerها در زبانی مثل #C کار کرده باشید، قطعا با IOC Container فراهم شده توسط Angular هم مشکلی نخواهید داشت.
اما یک مشکل! در زبانی مثل #C که همهی متغیرهای دارای نوع هستند، IOC Container با استفاده از Reflection، نوع پارامترهای درخواستی توسط سازندهی کلاس را بررسی کرده و با توجه به اطلاعاتی که ما از قبل در دسترس آن قرار داده بودیم، نمونهی مناسب آن را در اختیار در خواست کننده میگذارد.
اما در زبان جاوا اسکریپت که متغیرها دارای نوع نیستند، این کار به چه شکل انجام میگیرد؟
Angular برای این کار از نام پارامترها استفاده میکند. برای مثال Angular از نام پارامتر scope$ میفهمد که باید چه نمونه ای را به کلاس تزریق کند. پس نام پارامترها در سیستم تزریق وابستگی Angular نقش مهمی را ایفا میکنند.
اما در زبان جاوا اسکریپت، به طور پیش فرض امکانی برای به دست آوردن نام پارامترهای یک تابع وجود ندارد؛ پس Angular چگونه نام پارامترها را به دست میآورد؟ جواب در سورس کد Angular و در تابعی به نام annotate نهفته است که اساس کار این تابع استفاده از چهار عبارت با قاعده (Regular Expression) زیر است.
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; var FN_ARG_SPLIT = /,/; var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
تابع annotate تابعی را به عنوان پارامتر دریافت میکند و سپس با فراخواندن متد toString آن، کدهای آن تابع را به شکل یک رشته در میآورد. حال کدهای تابع را که اکنون به شکل یک رشته در دسترس است را با استفاده از عبارات با قاعدهی فوق پردازش میکند تا نام پارامترها را به دست آورد. در ابتدا کامنتهای موجود در تابع را حذف میکند، سپس نام پارامترها را استخراج میکند و با استفاده از "," آنها را جدا میکند و در نهایت نام پارامترها را در یک آرایه باز میگرداند.
استفاده از تزریق وابستگی، امکان نوشتن کدهایی با قابلیت استفاده مجدد و نوشتن سادهتر آزمونهای واحد را فراهم میکند. به خصوص کدهایی که با سرور ارتباط برقرار میکنند را میتوان به یک سرویس انتقال داد و از طریق تزریق وابستگی، از آن در کنترلر استفاده کرد. سپس در آزمونهای واحد میتوان قسمت ارتباط با سرور را با یک نمونه فرضی جایگزین کرد تا برای تست، احتیاجی به راه اندازی یک وب سرور واقعی و یا مرورگر نباشد.
Directives
یکی از مزیتهای Angular این است که قالبها را میتوان با HTML نوشت و این را باید مدیون موتور قدرتمند تبدیل گر DOM بدانیم که در آن گنجانده شده است و به شما این امکان را میدهد تا گرامر HTML را گسترش دهید.
تا به این جای کار با attributeهای زیادی در قالب HTML روبرو شدید که متعلق به HTML نیست. به طور مثال: جفت آکولادها که برای انقیاد داده به کار برده میشود، ng-app که برای مشخص کردن بخشی که باید توسط Angular کامپایل شود، ng-controller که برای مشخص کردن این که کدام بخش از View متعلق به کدام Controller است و ... تمامی Directiveهای پیش فرض Angular هستند.
با استفاده از Directiveها میتوانید عناصر و خاصیتها و حتی رویدادهای سفارشی برای HTML بنویسید؛ اما واقعا چه احتیاجی به تعریف عنصر سفارشی و توسعه گرامر HTML وجود دارد؟
HTML یک زبان طراحی است که در ابتدا برای تولید اسناد ایستا به وجود آمد و هیچ وقت هدفش تولید وب سایتهای امروزی که کاملا پویا هستند نبود. این امر تا جایی پیش رفته است که HTML را از یک زبان طراحی تبدیل به یک زبان برنامه نویسی کرده است و احتیاج به چنین زبانی کاملا مشهود است. به همین دلیل جامعهی وب مفهومی را به نام Web Components مطرح کرده است. Web Components به شما امکان تعریف عناصر HTML سفارشی را میدهد. برای مثال شما یک تگ سفارشی به نام datepicker مینویسید که دارای رفتار و ویژگیهای خاص خود است و به راحتی عناصر HTML رابا استفاده از آن توسعه میدهید. مطمئنا آیندهی وب این گونه است، اما هنوز خیلی از مرورگرها از این ویژگی پشتیبانی نمیکنند.
یکی دیگر از معادلهای Web Componentهای امروز را میتوان ویجتهای jQuery UI دانست. اگر بخواهم تعریفی از ویجت ارائه دهم به این گونه است که یک ویجت؛ کدهای HTML، CSS و javascript مرتبط به هم را کپسوله کرده است. مهمترین مزیت ویجت ها، قابلیت استفادهی مجدد آنهاست، به این دلیل که تمام منطق مورد نیاز را در خود کپسوله کرده است؛ برای مثال ویجت datepicker که به راحتی در برنامههای مختلف بدون احتیاج به نوشتن کدی قابل استفاده است.
خب، متاسفانه Web Componentها هنوز در دنیای وب امروزی رایج نشده اند و ویجتها هم آنچنان قدرت Web Componentها را ندارند. خب Angular با استفاده از امکان تعریف Directiveهای سفارشی به صورت cross-browser امکان تعریف عناصر سفارشیه همانند web Componentها را به شما میدهد. حتی به عقیدهی عده ای Directiveها بسیار قدرتمندتر از Web Components عمل میکنند و راحتی کار با آنها بیشتر است.
با استفاده از Directiveها میتوانید عنصر HTML سفارشی مثل </ datepicker>، خاصیت سفارشی مثل ng-controller، رویداد سفارشی مثل ng-click را تعریف کنید و یا حتی حالت و اتفاقات رخ داده در برنامه را زیر نظر بگیرید.
و این یکی از دلایلی است که میگویند Angular دارای ویژگی forward-thinking است.
البته Directiveها یکی از قدرتمندترین امکانات فریم ورک AngularJS است و در آینده به صورت مفصل بر روی آن بحث خواهد شد.
سرویسها در AngularJS
حتما این جمله را در هنگام نوشتن برنامهها با الگوی طراحی MVC بارها و بارها شنیده اید که در Controllerها نباید منطق تجاری و پیچیده ای را پیاده سازی کرد و باید به قسمتهای دیگری به نام سرویسها منتقل شوند و سپس در سازندهی کلاس کنترلر به عنوان پارامتر تعریف شوند تا توسط Angular نمونهی مناسب آن به کنترلر تزریق شود. Controllerها نباید پیاده کنندهی هیچ منطق تجاری و یا اصطلاحا business برنامه باشد و باید از لایهی سرویس استفاده کنند و تنها وظیفهی کنترلر باید مشخص کردن انقیاد داده و حالت برنامه باشد.
دلیل استفاده از سرویسها در کنترلر ها، نوشتن سادهتر آزمونهای واحد و استفادهی مجدد از سرویسها در قسمتهای مختلف پروژه و یا حتی پروژههای دیگر است.
معمولا اعمال مرتبط در ارتباط با سرور را در سرویسها پیاده سازی میکنند تا بتوان در موقع نوشتن آزمونهای واحد یک نمونهی فرضی را خودمان ساخته و آن را به عنوان وابستگی به کنترلری که در حال تست آن هستیم تزریق کنیم، در غیر این صورت احتیاج به راه اندازی یک وب سرور واقعی برای نوشتن آزمونهای واحد و در نتیجه کند شدن انجام آزمون را در بر دارد. قابلیت استفادهی مجدد سرویس هم به این معناست که منطق پیاده سازی شده در آن نباید ربطی به رابط کاربری و ... داشته باشد. برای مثال یک سرویس به نام userService باید دارای متد هایی مثل دریافت لیست کاربران، افزودن کاربر و ... باشد و بدیهی است که از این سرویسها میشود در قسمتهای مختلف برنامه استفاده کرد. همچنین سرویسها در Angular به صورت Singleton در اختیار کنترلرها قرار میگیرند و این بدین معناست که یک نمونه از هر سرویس ایجاد شده و به بخشهای مختلف برنامه تزریق میشود.
مفاهیم پایه ای AngularJs به پایان رسید. در مقاله بعدی یک مثال تقریبا کامل را نوشته و با اجزای مختلف Angular بیشتر آشنا میشویم.
با تشکر از مهدی محزونی برای بازبینی مطلب
- نکات مطلب «چه زمانهایی یک برنامهی ASP.NET ری استارت میشود؟» را بررسی کنید.
- همچنین اگر هاست نامبرده تمام سایتها را با یک Application pool مدیریت میکند، کرش یکی از چند ده سایت دیگر میتواند سبب ریاستارت شدن سایت شما هم بشود؛ چون برنامهها از همه ایزوله نشدهاند. راه حل آن ایجاد یک Application pool مجزا به ازای هر سایت هست (توسط هاستدار).
- همچنین اگر هاست نامبرده تمام سایتها را با یک Application pool مدیریت میکند، کرش یکی از چند ده سایت دیگر میتواند سبب ریاستارت شدن سایت شما هم بشود؛ چون برنامهها از همه ایزوله نشدهاند. راه حل آن ایجاد یک Application pool مجزا به ازای هر سایت هست (توسط هاستدار).
ارتقاء به ASP.NET Core 3.0: دریافت مقادیر Routing پس از فعالسازی Endpoint routing
تا پیش از معرفی Endpoint routing، اطلاعات مسیریابی جاری را میشد از طریق یکی از روشهای زیر به دست آورد:
// services.AddSingleton<IActionContextAccessor, ActionContextAccessor>(); private readonly IActionContextAccessor _actionContext; public MyClass(IActionContextAccessor actionContext) { _actionContext = actionContext; } public string GetData() { var routes = _actionContext.ActionContext.RouteData; var val = routes.Values["action"]?.ToString() as string; return val; }
protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, DynamicPermissionRequirement requirement) { if (!(context.Resource is AuthorizationFilterContext mvcContext)) { return; } var actionDescriptor = mvcContext.ActionDescriptor; actionDescriptor.RouteValues.TryGetValue("area", out var areaName); var area = string.IsNullOrWhiteSpace(areaName) ? string.Empty : areaName;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace ASPNETCoreIdentitySample.Services.Identity { public class DynamicPermissionsAuthorizationHandler : AuthorizationHandler<DynamicPermissionRequirement> { private readonly IHttpContextAccessor _httpContextAccessor; public DynamicPermissionsAuthorizationHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } protected override async Task HandleRequirementAsync( AuthorizationHandlerContext context, DynamicPermissionRequirement requirement) { var routeData = _httpContextAccessor.HttpContext.GetRouteData(); var areaName = routeData?.Values["area"]?.ToString(); var area = string.IsNullOrWhiteSpace(areaName) ? string.Empty : areaName;
نظرات مطالب
معرفی Kendo UI
در حال مطالعه فریم ورکهای جاوااسکریپتی طی مقالات سایت جاری هستم
با توجه ب گستردگی این فرم ورکها مثل کندو ، آنگولار ، ناک اوت ، React ، Ember و ..
با توجه به زمان حال و توسعه ی برنامههای حاضر و گستردگی منابع برای هر کدام ، انتخاب بهتر کدام است و یا برای چه موردی از چه فریم ورکی استفاده میشود ؟
واقعا دچار اختلال در انتخاب شدم ، لطفا راهنمایی کنید. تشکر
با توجه ب گستردگی این فرم ورکها مثل کندو ، آنگولار ، ناک اوت ، React ، Ember و ..
با توجه به زمان حال و توسعه ی برنامههای حاضر و گستردگی منابع برای هر کدام ، انتخاب بهتر کدام است و یا برای چه موردی از چه فریم ورکی استفاده میشود ؟
واقعا دچار اختلال در انتخاب شدم ، لطفا راهنمایی کنید. تشکر
بله. اینجا بحث شده.
+ یک نمونه مثال از یک برنامهی Angular که از روش مبتنی بر کوکی ASP.NET Core برای اعتبارسنجی استفاده میکند (بجای استفادهی از JWT).
همانطور که در قسمت قبل نیز بررسی کردیم، ASP.NET Core امکان تزریق وابستگیهای متداول را در اکثر قسمتهای آن مانند کنترلرها، میانافزارها و غیره، میسر و پیش بینی کردهاست؛ اما همیشه و در تمام مکانهای یک برنامهی وب، امکان تزریق وابستگیها در سازندهی کلاسها وجود ندارد و مجبور به استفادهی از الگوی Service Locator میباشیم. در این قسمت این مکانهای ویژه را بررسی خواهیم کرد.
HttpContext و امکان دسترسی به Service Locatorها
در ASP.NET Core هر جائیکه دسترسی به HttpContext وجود داشته باشد، میتوان از الگوی Service Locator نیز توسط خاصیت HttpContext.RequestServices آن استفاده کرد. این خاصیت از نوع IServiceProvider قرار گرفتهی در فضای نام System است که در قسمت دوم آنرا بررسی کردیم. توسط این اینترفیس به متد object GetService(Type serviceType) دسترسی خواهیم یافت و برای کار با نگارشهای جنریک آن نیاز است فضای نام Microsoft.Extensions.DependencyInjection را مورد استفاده قرار داد:
در اینجا یک نمونه مثال را از کار با HttpContext.RequestServices، در یک اکشن متد ملاحظه میکنید.
استفاده از Service Locatorها در فیلترها
هرچند استفادهی از this.HttpContext.RequestServices در یک اکشن متد که کنترلر آن تزریق وابستگیهای در سازندهی کلاس را به صورت توکار پشتیبانی میکند، مزیت خاصی را به همراه ندارد و توصیه نمیشود، اما در انتهای قسمت قبل، امکان تزریق وابستگیهای متداول در فیلترها را نیز بررسی کردیم. زمانیکه کار تزریق وابستگیها در سازندهی یک فیلتر صورت میگیرد، دیگر نمیتوان ApiExceptionFilter را به نحو متداول [ApiExceptionFilter] فراخوانی کرد؛ چون پارامترهای سازندهی آن جزو ثوابت قابل کامپایل نیستند و کامپایلر سیشارپ چنین اجازهای را نمیدهد. به همین جهت مجبور به استفادهی از [ServiceFilter(typeof(ApiExceptionFilter))] برای معرفی یک چنین فیلترهایی هستیم. اما میتوان این وضعیت را با استفاده از الگوی Service Locator بهبود بخشید. اینبار بجای تعریف وابستگیها در سازندهی یک فیلتر:
میتوان آنها را به صورت زیر نیز دریافت کرد:
در اینجا برای مثال سرویس ILogger توسط context.HttpContext.RequestServices قابل دسترسی شدهاست. به این ترتیب با حذف پارامترهای سازندهی این کلاس فیلتر که به صورت ثوابت زمان کامپایل قابل تعریف نیستند، امکان استفادهی از آن به صورت متداول [ApiExceptionFilter] میسر میشود.
استفاده از Service Locatorها در ValidationAttributes
روش تزریق وابستگیها در سازندهی کلاسهای ValidationAttribute مهیا نیست و امکانی مانند ServiceFilterها در اینجا کار نمیکند. به همین جهت تنها روشی که برای دسترسی به سرویسها باقی میماند استفاده از الگوی Service Locator است که مثالی از آنرا در کدهای زیر از طریق ValidationContext مشاهده میکنید:
استفاده از Service Locatorها در متد Main کلاس Program
فرض کنید سرویسی را در متد ConfigureServices کلاس Startup یک برنامهی وب ثبت کردهاید:
برای استفادهی از این سرویس در متد Main کلاس Program میتوان به صورت زیر عمل کرد:
متد Build در اینجا، یک وهلهی از نوع IWebHost را بازگشت میدهد. یکی از خواص این اینترفیس نیز Services از نوع IServiceProvider است:
زمانیکه به IServiceProvider دسترسی داشته باشیم، میتوان از متدهای GetRequiredService و یا GetService آن که در قسمت دوم، تفاوتهای آنها را بررسی کردیم، استفاده کرد و به وهلههای سرویسهای مدنظر دسترسی یافت.
استفاده از Service Locatorها در متد ConfigureServices کلاس Startup
برای دسترسی به سرویسهای برنامه در متد ConfigureServices میتوان متد BuildServiceProvider را بر روی پارامتر services فراخوانی کرد. خروجی آن از نوع کلاس ServiceProvider است که امکان دسترسی به متدهایی مانند GetRequiredService را میسر میکند:
در بسیاری از موارد، کار با GetRequiredService کافی است و مرحلهی بعدی هم ندارد. اما اگر سرویس شما دارای طول عمر از نوع Scoped و همچنین IDispoable نیز بود، همانطور که در نکتهی «روش صحیح Dispose اشیایی با طول عمر Scoped، در خارج از طول عمر یک درخواست ASP.NET Core» قسمت سوم عنوان شد، نیاز است یک Scope صریح را برای آن ایجاد و سپس آنرا به نحو صحیحی Dispose کرد که روش آنرا در مثال فوق ملاحظه میکنید.
استفاده از Service Locatorها در متد Configure کلاس Startup
در قسمت قبل عنوان شد که میتوان سرویسهای مدنظر خود را به صورت پارامترهایی جدید به متد Configure اضافه کرد و کار وهله سازی آنها توسط Service Provider برنامه به صورت خودکار صورت میگیرد:
در اینجا روش دومی نیز وجود دارد. میتوان از پارامتر app نیز به صورت Service Locator استفاده کرد:
خاصیت app.ApplicationServices از نوع IServiceProvider است و مابقی نکات آن با توضیحات «استفاده از Service Locatorها در متد ConfigureServices کلاس Startup» مطلب جاری دقیقا یکی است.
HttpContext و امکان دسترسی به Service Locatorها
در ASP.NET Core هر جائیکه دسترسی به HttpContext وجود داشته باشد، میتوان از الگوی Service Locator نیز توسط خاصیت HttpContext.RequestServices آن استفاده کرد. این خاصیت از نوع IServiceProvider قرار گرفتهی در فضای نام System است که در قسمت دوم آنرا بررسی کردیم. توسط این اینترفیس به متد object GetService(Type serviceType) دسترسی خواهیم یافت و برای کار با نگارشهای جنریک آن نیاز است فضای نام Microsoft.Extensions.DependencyInjection را مورد استفاده قرار داد:
using Microsoft.Extensions.DependencyInjection; namespace CoreIocSample02.Controllers { public class HomeController : Controller { public IActionResult Privacy() { var myDisposableService = this.HttpContext.RequestServices.GetRequiredService<IMyDisposableService>(); myDisposableService.Run(); return View(); } } }
استفاده از Service Locatorها در فیلترها
هرچند استفادهی از this.HttpContext.RequestServices در یک اکشن متد که کنترلر آن تزریق وابستگیهای در سازندهی کلاس را به صورت توکار پشتیبانی میکند، مزیت خاصی را به همراه ندارد و توصیه نمیشود، اما در انتهای قسمت قبل، امکان تزریق وابستگیهای متداول در فیلترها را نیز بررسی کردیم. زمانیکه کار تزریق وابستگیها در سازندهی یک فیلتر صورت میگیرد، دیگر نمیتوان ApiExceptionFilter را به نحو متداول [ApiExceptionFilter] فراخوانی کرد؛ چون پارامترهای سازندهی آن جزو ثوابت قابل کامپایل نیستند و کامپایلر سیشارپ چنین اجازهای را نمیدهد. به همین جهت مجبور به استفادهی از [ServiceFilter(typeof(ApiExceptionFilter))] برای معرفی یک چنین فیلترهایی هستیم. اما میتوان این وضعیت را با استفاده از الگوی Service Locator بهبود بخشید. اینبار بجای تعریف وابستگیها در سازندهی یک فیلتر:
public class ApiExceptionFilter : ExceptionFilterAttribute { private ILogger<ApiExceptionFilter> _logger; private IHostingEnvironment _environment; private IConfiguration _configuration; public ApiExceptionFilter(IHostingEnvironment environment, IConfiguration configuration, ILogger<ApiExceptionFilter> logger) { _environment = environment; _configuration = configuration; _logger = logger; }
using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.Extensions.Logging; namespace Filters { public class ApiExceptionFilter : ExceptionFilterAttribute { public override void OnException(ExceptionContext context) { var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<ApiExceptionFilter>>(); logger.LogError(context.Exception, context.Exception.Message); base.OnException(context); } } }
استفاده از Service Locatorها در ValidationAttributes
روش تزریق وابستگیها در سازندهی کلاسهای ValidationAttribute مهیا نیست و امکانی مانند ServiceFilterها در اینجا کار نمیکند. به همین جهت تنها روشی که برای دسترسی به سرویسها باقی میماند استفاده از الگوی Service Locator است که مثالی از آنرا در کدهای زیر از طریق ValidationContext مشاهده میکنید:
using Microsoft.Extensions.DependencyInjection; using System.ComponentModel.DataAnnotations; using CoreIocServices; namespace Test { public class CustomValidationAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var service = validationContext.GetRequiredService<IMyDisposableService>(); // use service // ... validation logic } } }
استفاده از Service Locatorها در متد Main کلاس Program
فرض کنید سرویسی را در متد ConfigureServices کلاس Startup یک برنامهی وب ثبت کردهاید:
namespace Test { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<ITokenGenerator, TokenGenerator>(); }
namespace Test { public class Program { public static void Main(string[] args) { IWebHost webHost = CreateWebHostBuilder(args).Build(); var tokenGenerator = webHost.Services.GetRequiredService<ITokenGenerator>(); string token = tokenGenerator.GetToken(); System.Console.WriteLine(token); webHost.Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); } }
namespace Microsoft.AspNetCore.Hosting { public interface IWebHost : IDisposable { IServiceProvider Services { get; } } }
استفاده از Service Locatorها در متد ConfigureServices کلاس Startup
برای دسترسی به سرویسهای برنامه در متد ConfigureServices میتوان متد BuildServiceProvider را بر روی پارامتر services فراخوانی کرد. خروجی آن از نوع کلاس ServiceProvider است که امکان دسترسی به متدهایی مانند GetRequiredService را میسر میکند:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { var scopeFactory = services.BuildServiceProvider().GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var provider = scope.ServiceProvider; using (var dbContext = provider.GetRequiredService<ApplicationDbContext>()) { // ... } } }
استفاده از Service Locatorها در متد Configure کلاس Startup
در قسمت قبل عنوان شد که میتوان سرویسهای مدنظر خود را به صورت پارامترهایی جدید به متد Configure اضافه کرد و کار وهله سازی آنها توسط Service Provider برنامه به صورت خودکار صورت میگیرد:
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app, IAmACustomService customService) { // .... } }
namespace CoreIocSample02 { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var provider = scope.ServiceProvider; using (var dbContext = provider.GetRequiredService<ApplicationDbContext>()) { //... } }
در مطلب «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول» با ساختار کلی یک پروژهی افزونهی پذیر ASP.NET MVC آشنا شدیم. پس از راه اندازی آن و مدتی کار کردن با این نوع پروژهها، این سؤال پیش خواهد آمد که ... خوب، اگر هر افزونه تصاویر یا فایلهای CSS و JS اختصاصی خودش را بخواهد داشته باشد، چطور؟ موارد عمومی مانند بوت استرپ و جیکوئری را میتوان در پروژهی پایه قرار داد تا تمام افزونهها به صورت یکسانی از آنها استفاده کنند، اما هدف، ماژولار شدن برنامه است و جدا کردن فایلهای ویژهی هر پروژه، از پروژهای دیگر و همچنین بالا بردن سهولت کار تیمی، با شکستن اجزای یک پروژه به صورت افزونههایی مختلف، بین اعضای یک تیم. در این قسمت نحوهی مدفون سازی انواع فایلهای استاتیک افزونهها را درون فایلهای DLL آنها بررسی خواهیم کرد. به این ترتیب دیگر نیازی به ارائهی مجزای آنها و یا کپی کردن آنها در پوشههای پروژهی اصلی نخواهد بود.
مدفون سازی فایلهای CSS و JS هر افزونه درون فایل DLL آن
به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاسهای عمومی و مشترک بین افزونهها استفاده خواهیم کرد. برای مثال قصد نداریم کلاسهای سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونهای جدید کپی کنیم. کتابخانهی Common، امکان استفادهی مجدد از یک سری کدهای تکراری را در بین افزونهها میسر میکند.
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.
پس از این مقدمات، کلاس ذیل را به این پروژهی class library جدید اضافه کنید:
اگر با سیستم bundling & minification کار کرده باشید، با تعاریفی مانند ("new Bundle("~/Plugin1/Scripts آشنا هستید. سازندهی کلاس Bundle، پارامتر دومی را نیز میپذیرد که از نوع IBundleTransform است. با پیاده سازی اینترفیس IBundleTransform میتوان محل ارائهی فایلهای استاتیک CSS و JS را بجای فایل سیستم متداول و پیش فرض، به منابع مدفون شدهی در اسمبلی جاری هدایت و تنظیم کرد.
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایلها و منابع مدفون شده گشته و سپس محتوای آنها را بازگشت میدهد.
اکنون برای استفادهی از آن، به پروژهی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژهی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آنرا به نحو ذیل تکمیل کنید:
در اینجا نحوهی کار با کلاس سفارشی EmbeddedResourceTransform را مشاهده میکنید.
ابتدا فایلهای js و سپس فایلهای css برنامه به سیستم Bundling برنامه اضافه شدهاند.
این فایلها به صورت ذیل در پروژه تعریف گردیدهاند:
همانطور که مشاهده میکنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آنها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.
یک نکتهی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نامهایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره میشوند:
مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن
مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
تصاویر پروژهی افزونه نیز به صورت embedded resource در اسمبلی آن قرار خواهند گرفت. به همین جهت باید سیستم مسیریابی را پس درخواست رسیدهی جهت نمایش تصاویر، به منابع ذخیره شدهی در اسمبلی آن هدایت نمود. اینکار را با پیاده سازی یک IRouteHandler سفارشی، میتوان به نحو فوق مدیریت کرد.
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت میدهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شدهاست و هدرهای خاص caching را به صورت خودکار اضافه میکند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه میکند.
اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
در مسیریابی تعریف شده، تمام درخواستهای رسیدهی به مسیر NewsArea/Images به EmbeddedResourceRouteHandler هدایت میشوند.
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی میشوند از نوع png یا jpg تعریف شدهاند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شدهاست.
استفادهی نهایی از تنظیمات فوق در یک View افزونه
پس از اینکه تصاویر و فایلهای css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آنها را نیز مشخص نمودیم، اکنون نوبت به استفادهی از آنها در یک View است:
در اینجا نحوهی تعریف فایلهای CSS و JS ارائه شدهی توسط سیستم Bundling را مشاهده میکنید.
همچنین مسیر تصویر مشخص شدهی در آن، اینبار یک NewsArea اضافهتر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفتهاست اما میخواهیم این درخواستها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.
اینبار اگر برنامه را اجرا کنیم، میتوان به سه نکته در آن دقت داشت:
الف) alert اجرا شده از فایل js مدفون شده خوانده شدهاست.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شدهاست.
ج) تصویر نمایش داده شده، همان تصویر مدفون شدهی در فایل DLL برنامه است.
و هیچکدام از این فایلها، به پوشههای پروژهی اصلی برنامه، کپی نشدهاند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part2.zip
مدفون سازی فایلهای CSS و JS هر افزونه درون فایل DLL آن
به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاسهای عمومی و مشترک بین افزونهها استفاده خواهیم کرد. برای مثال قصد نداریم کلاسهای سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونهای جدید کپی کنیم. کتابخانهی Common، امکان استفادهی مجدد از یک سری کدهای تکراری را در بین افزونهها میسر میکند.
این پروژه برای کامپایل شدن نیاز به بستهی نیوگت ذیل دارد:
PM> install-package Microsoft.AspNet.Web.Optimization
پس از این مقدمات، کلاس ذیل را به این پروژهی class library جدید اضافه کنید:
using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Web.Optimization; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceTransform : IBundleTransform { private readonly IList<string> _resourceFiles; private readonly string _contentType; private readonly Assembly _assembly; public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly) { _resourceFiles = resourceFiles; _contentType = contentType; _assembly = assembly; } public void Process(BundleContext context, BundleResponse response) { var result = new StringBuilder(); foreach (var resource in _resourceFiles) { using (var stream = _assembly.GetManifestResourceStream(resource)) { if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName)); } using (var reader = new StreamReader(stream)) { result.Append(reader.ReadToEnd()); } } } response.ContentType = _contentType; response.Content = result.ToString(); } } }
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایلها و منابع مدفون شده گشته و سپس محتوای آنها را بازگشت میدهد.
اکنون برای استفادهی از آن، به پروژهی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژهی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آنرا به نحو ذیل تکمیل کنید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { var executingAssembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var assemblyNameSpace = executingAssembly.GetName().Name; var scriptsBundle = new Bundle("~/Plugin1/Scripts", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Scripts.test1.js" }, "application/javascript", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { scriptsBundle.Transforms.Add(new JsMinify()); } bundles.Add(scriptsBundle); var cssBundle = new Bundle("~/Plugin1/Content", new EmbeddedResourceTransform(new List<string> { assemblyNameSpace + ".Content.test1.css" }, "text/css", executingAssembly)); if (!HttpContext.Current.IsDebuggingEnabled) { cssBundle.Transforms.Add(new CssMinify()); } bundles.Add(cssBundle); BundleTable.EnableOptimizations = true; } public void RegisterRoutes(RouteCollection routes) { } public void RegisterServices(IContainer container) { } } }
این فایلها به صورت ذیل در پروژه تعریف گردیدهاند:
همانطور که مشاهده میکنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آنها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.
یک نکتهی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نامهایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره میشوند:
مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن
مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System; using System.Collections.Generic; using System.Reflection; using System.Web; using System.Web.Routing; namespace MvcPluginMasterApp.Common.WebToolkit { public class EmbeddedResourceRouteHandler : IRouteHandler { private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration); } } public class EmbeddedResourceHttpHandler : IHttpHandler { private readonly RouteData _routeData; private readonly Assembly _assembly; private readonly string _resourcePath; private readonly TimeSpan _cacheDuration; public EmbeddedResourceHttpHandler( RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration) { _routeData = routeData; _assembly = assembly; _resourcePath = resourcePath; _cacheDuration = cacheDuration; } public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { var routeDataValues = _routeData.Values; var fileName = routeDataValues["file"].ToString(); var fileExtension = routeDataValues["extension"].ToString(); var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension); var stream = _assembly.GetManifestResourceStream(manifestResourceName); if (stream == null) { throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName)); } context.Response.Clear(); context.Response.ContentType = "application/octet-stream"; cacheIt(context.Response, _cacheDuration); stream.CopyTo(context.Response.OutputStream); } private static void cacheIt(HttpResponse response, TimeSpan duration) { var cache = response.Cache; var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic); if (maxAgeField != null) maxAgeField.SetValue(cache, duration); cache.SetCacheability(HttpCacheability.Public); cache.SetExpires(DateTime.Now.Add(duration)); cache.SetMaxAge(duration); cache.AppendCacheExtension("must-revalidate, proxy-revalidate"); } } }
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت میدهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شدهاست و هدرهای خاص caching را به صورت خودکار اضافه میکند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه میکند.
اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. var assembly = Assembly.GetExecutingAssembly(); // Mostly the default namespace and assembly name are the same var nameSpace = assembly.GetName().Name; var resourcePath = string.Format("{0}.Images", nameSpace); routes.Insert(0, new Route("NewsArea/Images/{file}.{extension}", new RouteValueDictionary(new { }), new RouteValueDictionary(new { extension = "png|jpg" }), new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30)) )); } } }
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی میشوند از نوع png یا jpg تعریف شدهاند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شدهاست.
استفادهی نهایی از تنظیمات فوق در یک View افزونه
پس از اینکه تصاویر و فایلهای css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آنها را نیز مشخص نمودیم، اکنون نوبت به استفادهی از آنها در یک View است:
@{ ViewBag.Title = "From Plugin 1"; } @Styles.Render("~/Plugin1/Content") <h2>@ViewBag.Message</h2> <div class="row"> Embedded image: <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" /> </div> @section scripts { @Scripts.Render("~/Plugin1/Scripts") }
همچنین مسیر تصویر مشخص شدهی در آن، اینبار یک NewsArea اضافهتر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفتهاست اما میخواهیم این درخواستها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.
اینبار اگر برنامه را اجرا کنیم، میتوان به سه نکته در آن دقت داشت:
الف) alert اجرا شده از فایل js مدفون شده خوانده شدهاست.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شدهاست.
ج) تصویر نمایش داده شده، همان تصویر مدفون شدهی در فایل DLL برنامه است.
و هیچکدام از این فایلها، به پوشههای پروژهی اصلی برنامه، کپی نشدهاند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part2.zip