البته این رو هم باید اضافه میکردید استفاده از این متد در حالت UnSafe(با سوئیچ UnSafe) قابل استفاده است به این دلیل که رشتهی رمزنگاری شده در حافظهی مدیریت نشده(خارج از کنترل CLR)قرار میگیرد.بنابراین دسترسی به این بخش تقریبا غیرممکن است.
موفق باشید.
گام 3 – از بین بردن ارتباط لایهها (Loose Coupling)
بجای استفاده از اشیاء واقعی، براساس interfaceها برنامه نویسی کنید. اگر شما کد خود را با استفاده از IShoppingCartService به عنوان یک interface بجای استفاده از شیء واقعی ShoppingCartService نوشته باشید، زمانیکه تست را مینویسید، میتوانید یک سرویس کارت خرید جعلی (mocking) که IShoppingCartService را پیاده سازی کرده جایگزین شیء اصلی نمایید. در کد زیر، توجه کنید تنها تغییر این است که متغیر عضو اکنون از نوع IShoppingCartService است بجای ShoppingCartService .
public interface IShoppingCartService { ShoppingCart GetContents(); ShoppingCart AddItemToCart(int itemId, int quantity); } public class ShoppingCartService : IShoppingCartService { public ShoppingCart GetContents() { throw new NotImplementedException("Get cart from Persistence Layer"); } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException("Add Item to cart then return updated cart"); } } public class ShoppingCart { public List<product> Items { get; set; } } public class Product { public int ItemId { get; set; } public string ItemName { get; set; } } public class ShoppingCartController : Controller { //Concrete object below points to actual service //private ShoppingCartService _shoppingCartService; //loosely coupled code below uses the interface rather than the //concrete object private IShoppingCartService _shoppingCartService; public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } public ActionResult GetCart() { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.GetContents(); return View("Cart", cart); } public ActionResult AddItemToCart(int itemId, int quantity) { //now using the shared instance of the shoppingCartService dependency ShoppingCart cart = _shoppingCartService.AddItemToCart(itemId, quantity); return View("Cart", cart); } }
گام 4 – وابستگیها را تزریق کنید
اکنون ما تمام وابستگیها را در یک جا مرکزیت دادهایم و کد ما رابطه کمی با آن وابستگیها دارد. همانند گذشته، چندین راه برای پیاده سازی این گام وجود دارد. بدون استفاده از ابزارهای کمکی برای این مفهوم، سادهترین راه دوباره نویسی (Overload) متد ایجاد کننده است:
//loosely coupled code below uses the interface rather //than the concrete object private IShoppingCartService _shoppingCartService; //MVC uses this constructor public ShoppingCartController() { _shoppingCartService = new ShoppingCartService(); } //You can use this constructor when testing to inject the //ShoppingCartService dependency public ShoppingCartController(IShoppingCartService shoppingCartService) { _shoppingCartService = shoppingCartService; }
گام 5 – تست را با استفاده از یک شیء جعلی (Mocking) انجام دهید
مثالی از یک سناریوی تست ممکن در زیر آمده است. توجه کنید که یک شیء جعلی از نوع کلاس ShoppingCartService ساختهایم. این شیء جعلی فرستاده میشود به شیء Controller و متد GetContents پیاده سازی میشود تا بجای آنکه کد اصلی را که به منبع داده مراجعه میکند اجرا نماید، دادههای جعلی و شبیه سازی شده را برگرداند. بدلیل آنکه تمام کد را نوشته ایم، بسیار سریعتر از اجرای کوئری بر روی دیتابیس اجرا خواهد شد و دیگر نگرانی بابت تهیه داده تستی و یا تصحیح داده بعد از اتمام تست (بازگرداندن داده به حالت قبل از تست) نخواهم داشت. توجه داشته باشید که بدلیل مرکزیت دادن به وابستگیها در گام 2 ، تنها باید یکبار آنرا تزریق نماییم و بخاطر کاری که در گام 3 انجام شد، وابستگی ما به حدی پایین آمده که میتوانیم هر شیءایی را (جعلی و یا حقیقی) ارسال کنیم و فقط کافیست شیء مورد نظر IShoppingCartService را پیاده سازی کرده باشد.
[TestClass] public class ShoppingCartControllerTests { [TestMethod] public void GetCartSmokeTest() { //arrange ShoppingCartController controller = new ShoppingCartController(new ShoppingCartServiceStub()); // Act ActionResult result = controller.GetCart(); // Assert Assert.IsInstanceOfType(result, typeof(ViewResult)); } } /// <summary> /// This is is a stub of the ShoppingCartService /// </summary> public class ShoppingCartServiceStub : IShoppingCartService { public ShoppingCart GetContents() { return new ShoppingCart { Items = new List<product> { new Product {ItemId = 1, ItemName = "Widget"} } }; } public ShoppingCart AddItemToCart(int itemId, int quantity) { throw new NotImplementedException(); } }
مطالب تکمیلی
از یک ابزار کنترل وابستگی (IoC/DI) استفاده کنید:
از رایجترین و عمومیترین ابزارهای کنترل وابستگی برای .Net میتوان به StructureMap و CastleWindsor اشاره کرد. در کد نویسی واقعی، شما وابستگیهای بسیاری خواهید داشت، که این وابستگیها هم وابستگی هایی دارند که به سرعت از مدیریت شما خارج خواهند شد. راه حل این مشکل استفاده از یک ابزار کنترل وابستگی خواهد بود.
از یک چارچوب تجزیه (Isolation Framework) استفاده نمایید:
برای ایجاد اشیاء جعلی ممکن است کار زیادی لازم باشد و استفاده از یک Isolation Framework میتواند زمان و میزان کد نویسی شمارا کم کند. از رایجترین این ابزارها میتوان Rhino Mocks و Moq را نام برد.
در Angular امکان تعریف مسیریابیهایی، درون سایر مسیریابیها نیز پیش بینی شدهاست. با استفاده از مفهوم Child Routes، امکان تعریف سلسله مراتب مسیریابیها جهت ساماندهی و مدیریت مسیریابی درون برنامه، وجود دارد. همچنین lazy loading مسیریابیها را نیز سادهتر کرده و کارآیی آغاز برنامه را بهبود میبخشند.
علت نیاز به Child Routes
در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شدهاست:
سپس از دایرکتیو router-outlet جهت تعریف محل قرارگیری محتوای این مسیریابیها استفاده شدهاست:
هربار که مسیری تغییر میکند، محتوای router-outlet با محتوای قالب آن کامپوننت جایگزین خواهد شد. اما اگر تعداد المانهای صفحهی ویرایش محصولات بیش از اندازه بودند و خواستیم فیلدهای آنرا به دو برگه (tab) تقسیم کنیم چطور؟ برای اینکار نیاز است تا router-outlet ثانویه و مخصوص این قالب را تعریف کنیم. هربار که کاربری بر روی برگهای کلیک میکند، به کمک Child routes، محتوای آن برگه را در این router-outlet ثانویه نمایش میدهیم. به این ترتیب به کمک Child routes میتوان امکان نمایش محتوای مسیریابی دیگری را درون مسیریابی اصلی، میسر کرد.
کاربردهای Child routes
- امکان تقسیم فرمهای طولانی به چند Tab
- امکان طراحی طرحبندیهای Master/Layout
- قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
- بهبود کپسوله سازی ماژولهای برنامه
- جزو الزامات Lazy loading هستند
تنظیم کردن Child Routes
مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحهی خوشآمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالبهای کامپوننتهای اینها نیز در router-outlet اصلی برنامه نمایش داده میشوند. در ادامه میخواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالبهای کامپوننتهای هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه میکنیم:
این دستورات سبب به روز رسانی فایل src\app\product\product.module.ts، جهت تکمیل قسمت declarations آن نیز خواهند شد.
به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایهی برچسبهای یک محصول تکمیل میکنیم:
در این حالت میتوانید فایل app\product\product-data.ts را نیز ویرایش کرده و به هر محصول، تعدادی گروه و برچسب را نیز انتساب دهید؛ که البته ذکر tags آن اختیاری است. در اینجا فایل src\app\product\product.service.ts نیز باید ویرایش شده و متد initializeProduct آن تعاریف []:category: null, tags را نیز پیدا کنند.
در ادامه برای تنظیم Child Routes، فایل src\app\product\product-routing.module.ts را گشوده و آنرا به نحو ذیل تکمیل کنید:
- Child Routes، در داخل آرایهی خاصیت children تنظیمات یک مسیریابی والد، قابل تعریف هستند. برای نمونه در اینجا Child Routes به تنظیمات مسیریابی ویرایش محصولات اضافه شدهاند و کار توسعهی مسیریابی والد خود را انجام میدهند.
- در اولین Child Route تعریف شده، مقدار path به '' تنظیم شدهاست. به این ترتیب مسیریابی پیش فرض آن (در صورت عدم ذکر صریح آنها در URL) به صورت خودکار به مسیریابی info هدایت خواهد شد. بنابراین درخواست مسیر products/:id/edit به دومین Child Route تنظیم شده هدایت میشود.
- دومین Child Route تعریف شده با مسیری مانند products/:id/edit/info تطابق پیدا میکند.
- سومین Child Route تعریف شده با مسیری مانند products/:id/edit/tags تطابق پیدا میکند.
تعیین محل نمایش Child Views
برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوهی تعریف آن با primary outlet تعریف شدهی در فایل src\app\app.component.html تفاوتی ندارد.
برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آنرا به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه میخواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگهی جدید جایگزین میکنیم. در اینجا نحوهی تعریف لینکهای جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده میکنید:
تا اینجا اگر برنامه را توسط دستور ng s -o اجرا کنید، صفحهی ویرایش محصول اول، چنین شکلی را پیدا کردهاست:
فعالسازی Child Routes
دو روش برای فعالسازی Child Routes وجود دارند:
الف) با ذکر مسیر مطلق
در این حالت تمام URL segments این مسیر باید به عنوان پارامترهای لینک قید شوند.
ب) با ذکر مسیر نسبی
این مسیر از URL segment جاری شروع میشود و نباید در حین تعریف آن از / استفاده کرد. اگر از / استفاده شود، معنای ذکر مسیری مطلق را میدهد.
در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار میکند).
دقیقا همین پارامترها، قابلیت استفادهی در متد this.route.navigate را نیز دارند:
الف) برای حالت ذکر مسیر مطلق:
ب) و برای حالت ذکر مسیر نسبی:
در حالت ذکر مسیر نسبی، نیاز است پارامتر اضافهی دیگری را جهت مشخص سازی مسیریابی والد نیز قید کرد.
تکمیل Child Viewهای برنامه
تا اینجا لینکهایی نسبی را به مسیریابیهای info و tags اضافه کردیم. در ادامه قالبها و کامپوننتهای آنها را تکمیل میکنیم:
الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
با قالب src\app\product\product-edit\product-edit.component.html که در حقیقت همان فرمی است که از کامپوننت والد حذف کردیم و به اینجا منتقل شدهاست:
ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
با قالب src\app\product\product-edit-tags\product-edit-tags.component.html
دریافت اطلاعات جهت Child Routes
روشهای متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
الف) میتوان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبل نیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن میشود.
ب) میتوان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آنها از وب سرور، دریافت کرد.
ج) اگر قسمتهای مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگههای مختلف آن به اشتراک گذاشته شوند، این اطلاعات را میتوانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
در این مثال ما هرچند چندین برگهی مختلف را طراحی کردهایم، اما اطلاعات نمایش داده شدهی توسط آنها متعلق به یک شیء محصول میباشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننتهای مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهلهی آن کار کرد. به همین جهت با this.route.parent در هر یک از Child Components تعریف شده کار میکنیم تا بتوان به یک وهلهی شیء محصول، دسترسی یافت.
د) همچنین میتوان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحهی افزودن محصولات با صفحهی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده میکنید.
در اینجا data['product'] به key/value تعریف شدهی resolve: { product: ProductResolverService } در تنظیمات مسیریابی اشاره میکند که آنرا در قسمت قبل تکمیل کردیم.
شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفتهایم و در آنجا نیز با شیء this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار میکنیم. به این ترتیب مطمئن خواهیم شد که this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره میکنند.
به این ترتیب دکمهی Save ذیل هر دو برگه، به درستی عمل کرده و میتواند اطلاعات نهایی یک شیء محصول را ذخیره کند.
رفع مشکلات اعتبارسنجی فرمهای قرار گرفتهی در برگههای مختلف
علت استفادهی از ViewChild در ProductEditInfoComponent
که به فرم قالب آن اشاره میکند:
این است که بتوان متد this.productForm.reset آنرا پس از هربار دریافت اطلاعات از سرور، فراخوانی کرد. این متد نه تنها اطلاعات آنرا پاک میکند، بلکه خطاهای اعتبارسنجی آنرا نیز به حالت نخست برمیگرداند. بنابراین در این حالت اگر سبب بروز یک خطای اعتبارسنجی، در فرم ویرایش اطلاعات شویم و در همان لحظه صفحهی افزودن یک محصول جدید را درخواست کنیم، کاربر همان خطای اعتبارسنجی قبلی را مجددا مشاهده نکرده و یک فرم از ابتدا آغاز شده را مشاهده میکند.
انجام اینکار برای برگههای دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آنها از حافظه خارج میشوند و مجددا بازیابی خواهند شد.
مشکل دوم اعتبارسنجی این فرم چند برگهای این است که هرچند خالی کردن نام یا کد محصول، سبب نمایش خطای اعتبارسنجی میشود، اما سبب غیرفعال شدن دکمهی Save نخواهند شد؛ از این جهت که این دکمه در قالب والد قرار دارد و نه در قالب فرزندان.
در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده میشود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف میشود و با قالب کامپوننت edit tags جایگزین میشود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload میشوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگهی ویرایش اطلاعات را میکند، قالب edit tags و اطلاعات اعتبارسنجی آن unload میشوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
راه حلهای ممکن:
الف) بدنهی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المانهای فرمهای مرتبط را ارائه دهند. این روش کار نمیکند چون Angular المانهای فرمهای قرار گرفتهی درون router-outlet را شناسایی نمیکند.
ب) قرار دادن فرمها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرمهای فرزند آنرا در اینجا ملاحظه میکنید:
- در اینجا dataIsValid، به صورت key/value تعریف شدهاست که در آن key، مسیر یک برگه و مقدار آن، معتبر بودن یا غیرمعتبر بودن وضعیت اعتبارسنجی آن است.
- سپس متد validate اضافه شدهاست تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شدهاست برای انجام اعتبارسنجی استفاده میکنیم. از این جهت که برگهها نیز با استفاده از this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگهها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس میشود.
- متد isValid، مسیر هر برگه را دریافت میکند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز میگرداند. اگر path در اینجا قید نشود، وضعیت تمام برگهها بررسی میشوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
- در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمهی ثبت را نیز به این متد متصل میکنیم:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
علت نیاز به Child Routes
در مثال این سری، منوی اصلی آن به صورت ذیل تعریف شدهاست:
<ul class="nav navbar-nav"> <li><a [routerLink]="['/home']">Home</a></li> <li><a [routerLink]="['/products']">Product List</a></li> <li><a [routerLink]="['/products', 0, 'edit']">Add Product</a></li> </ul>
<div class="container"> <router-outlet></router-outlet> </div>
کاربردهای Child routes
- امکان تقسیم فرمهای طولانی به چند Tab
- امکان طراحی طرحبندیهای Master/Layout
- قرار دادن قالب یک کامپوننت، درون قالب کامپوننتی دیگر
- بهبود کپسوله سازی ماژولهای برنامه
- جزو الزامات Lazy loading هستند
تنظیم کردن Child Routes
مثال جاری این سری، تنها به همراه یک سری primary routes است؛ مانند صفحهی خوشآمد گویی، نمایش لیست محصولات، افزودن و ویرایش محصولات. قالبهای کامپوننتهای اینها نیز در router-outlet اصلی برنامه نمایش داده میشوند. در ادامه میخواهیم کامپوننت ویرایش محصولات را تغییر داده و تعدادی برگه را به آن اضافه کنیم. برای اینکار، نیاز به تعریف Child routes است تا بتوان قالبهای کامپوننتهای هر برگه را در router-outlet کامپوننت والد که در درون router-outlet اصلی برنامه قرار دارد، نمایش داد.
به همین جهت دو کامپوننت جدید ProductEditInfo و ProductEditTags را نیز به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductEditInfo >ng g c product/ProductEditTags
به علاوه اینترفیس src\app\product\iproduct.ts را نیز جهت افزودن گروه محصولات و همچنین آرایهی برچسبهای یک محصول تکمیل میکنیم:
export interface IProduct { id: number; productName: string; productCode: string; category: string; tags?: string[]; }
در ادامه برای تنظیم Child Routes، فایل src\app\product\product-routing.module.ts را گشوده و آنرا به نحو ذیل تکمیل کنید:
import { ProductEditTagsComponent } from './product-edit-tags/product-edit-tags.component'; import { ProductEditInfoComponent } from './product-edit-info/product-edit-info.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolverService } }, { path: 'products/:id/edit', component: ProductEditComponent, resolve: { product: ProductResolverService }, children: [ { path: '', redirectTo: 'info', pathMatch: 'full' }, { path: 'info', component: ProductEditInfoComponent }, { path: 'tags', component: ProductEditTagsComponent } ] } ];
- در اولین Child Route تعریف شده، مقدار path به '' تنظیم شدهاست. به این ترتیب مسیریابی پیش فرض آن (در صورت عدم ذکر صریح آنها در URL) به صورت خودکار به مسیریابی info هدایت خواهد شد. بنابراین درخواست مسیر products/:id/edit به دومین Child Route تنظیم شده هدایت میشود.
- دومین Child Route تعریف شده با مسیری مانند products/:id/edit/info تطابق پیدا میکند.
- سومین Child Route تعریف شده با مسیری مانند products/:id/edit/tags تطابق پیدا میکند.
تعیین محل نمایش Child Views
برای نمایش قالب یک Child Route درون قالب والد آن، نیاز به تعریف یک دایرکتیو router-outlet جدید، درون قالب والد است و نحوهی تعریف آن با primary outlet تعریف شدهی در فایل src\app\app.component.html تفاوتی ندارد.
برای پیاده سازی این مفهوم، نیاز است از قالب ویرایش محصولات و یا فایل src\app\product\product-edit\product-edit.component.html که قالب والد این Child Routes است شروع و آنرا به دو Child View تقسیم کنیم. این قالب، تاکنون حاوی فرمی جهت ویرایش و افزودن محصولات است. در ادامه میخواهیم بجای آن چند برگه را نمایش دهیم. به همین جهت این فرم را حذف کرده و با دو برگهی جدید جایگزین میکنیم. در اینجا نحوهی تعریف لینکهای جدید، به Child Routes و همچنین محل قرارگیری router-outlet ثانویه را نیز مشاهده میکنید:
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <div class="wizard"> <a [routerLink]="['info']"> Basic Information </a> <a [routerLink]="['tags']"> Search Tags </a> </div> <router-outlet></router-outlet> </div> <div class="panel-footer"> <div class="row"> <div class="col-md-6 col-md-offset-2"> <span> <button class="btn btn-primary" type="button" style="width:80px;margin-right:10px" [disabled]="!isValid()" (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> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div>
فعالسازی Child Routes
دو روش برای فعالسازی Child Routes وجود دارند:
الف) با ذکر مسیر مطلق
<a [routerLink]="['/products',product.id,'edit','info']">Info</a>
ب) با ذکر مسیر نسبی
<a [routerLink]="['info']">Info</a>
در این حالت اگر تنظیمات والد این مسیریابی تغییر کنند، نیازی به تغییر مسیر نسبی تعریف شده نیست (برخلاف حالت مطلق که بر اساس قید کامل تمام اجزای مسیریابی والد آن کار میکند).
دقیقا همین پارامترها، قابلیت استفادهی در متد this.route.navigate را نیز دارند:
الف) برای حالت ذکر مسیر مطلق:
this.router.navigate(['/products', this.product.id,'edit','info']);
this.router.navigate(['info', { relativeTo: this.route }]);
تکمیل Child Viewهای برنامه
تا اینجا لینکهایی نسبی را به مسیریابیهای info و tags اضافه کردیم. در ادامه قالبها و کامپوننتهای آنها را تکمیل میکنیم:
الف) تکمیل کامپوننت ProductEditInfoComponent در فایل src\app\product\product-edit\product-edit.component.ts
import { ActivatedRoute } from '@angular/router'; import { NgForm } from '@angular/forms'; import { Component, OnInit, ViewChild } from '@angular/core'; import { IProduct } from './../iproduct'; @Component({ //selector: 'app-product-edit-info', templateUrl: './product-edit-info.component.html', styleUrls: ['./product-edit-info.component.css'] }) export class ProductEditInfoComponent implements OnInit { @ViewChild(NgForm) productForm: NgForm; errorMessage: string; product: IProduct; constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; if (this.productForm) { this.productForm.reset(); } }); } }
<div class="panel-body"> <form class="form-horizontal" novalidate #productForm="ngForm"> <fieldset> <legend>Basic Product Information</legend> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty || product.id !== 0) && !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 || product.id !== 0) && 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 || product.id !== 0) && !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 || product.id !== 0) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </fieldset> </form> </div>
ب) تکمیل کامپوننت ProductEditTagsComponent در فایل src\app\product\product-edit-tags\product-edit-tags.component.ts
import { ActivatedRoute } from '@angular/router'; import { IProduct } from './../iproduct'; import { Component, OnInit } from '@angular/core'; @Component({ //selector: 'app-product-edit-tags', templateUrl: './product-edit-tags.component.html', styleUrls: ['./product-edit-tags.component.css'] }) export class ProductEditTagsComponent implements OnInit { errorMessage: string; newTags = ''; product: IProduct; constructor(private route: ActivatedRoute) { } ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; }); } // Add the defined tags addTags(): void { let tagArray = this.newTags.split(','); this.product.tags = this.product.tags ? this.product.tags.concat(tagArray) : tagArray; this.newTags = ''; } // Remove the tag from the array of tags. removeTag(idx: number): void { this.product.tags.splice(idx, 1); } }
<div class="panel-body"> <form class="form-horizontal" novalidate> <fieldset> <legend>Product Search Tags</legend> <div class="form-group" [ngClass]="{'has-error': (categoryVar.touched || categoryVar.dirty || product.id !== 0) && !categoryVar.valid }"> <label class="col-md-2 control-label" for="categoryId">Category</label> <div class="col-md-8"> <input class="form-control" id="categoryId" type="text" placeholder="Category (required)" required minlength="3" [(ngModel)]="product.category" name="category" #categoryVar="ngModel" /> <span class="help-block" *ngIf="(categoryVar.touched || categoryVar.dirty || product.id !== 0) && categoryVar.errors"> <span *ngIf="categoryVar.errors.required"> A category must be entered. </span> <span *ngIf="categoryVar.errors.minlength"> The category must be at least 3 characters in length. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (tagVar.touched || tagVar.dirty || product.id !== 0) && !tagVar.valid }"> <label class="col-md-2 control-label" for="tagsId">Search Tags</label> <div class="col-md-8"> <input class="form-control" id="tagsId" type="text" placeholder="Search keywords separated by commas" minlength="3" [(ngModel)]="newTags" name="tags" #tagVar="ngModel" /> <span class="help-block" *ngIf="(tagVar.touched || tagVar.dirty || product.id !== 0) && tagVar.errors"> <span *ngIf="tagVar.errors.minlength"> The search tag must be at least 3 characters in length. </span> </span> </div> <div class="col-md-1"> <button type="button" class="btn btn-default" (click)="addTags()"> Add </button> </div> </div> <div class="row col-md-8 col-md-offset-2"> <span *ngFor="let tag of product.tags; let i = index"> <button class="btn btn-default" style="font-size:smaller;margin-bottom:12px" (click)="removeTag(i)"> {{tag}} <span class="glyphicon glyphicon-remove"></span> </button> </span> </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </fieldset> </form> </div>
دریافت اطلاعات جهت Child Routes
روشهای متعددی برای دریافت اطلاعات جهت Child Routes وجود دارند:
الف) میتوان از متد this.productService.getProduct جهت دریافت اطلاعات یک محصول استفاده کرد. اما همانطور که در قسمت قبل نیز بررسی کردیم، این روش سبب نمایش ابتدایی یک قالب خالی و پس از مدتی، نمایش اطلاعات آن میشود.
ب) میتوان توسط this.route.snapshot.data['product'] اطلاعات را از Route Resolver، پس از پیش واکشی آنها از وب سرور، دریافت کرد.
ج) اگر قسمتهای مختلف Child Routes قرار است با اطلاعاتی یکسان کار کنند که قرار است بین برگههای مختلف آن به اشتراک گذاشته شوند، این اطلاعات را میتوانند از Route Resolver والد خود به کمک this.route.snapshot.data['product'] دریافت کنند.
در این مثال ما هرچند چندین برگهی مختلف را طراحی کردهایم، اما اطلاعات نمایش داده شدهی توسط آنها متعلق به یک شیء محصول میباشند. بنابراین نیاز است بتوان این اطلاعات را بین کامپوننتهای مختلف این Child Routes به اشتراک گذاشت و تنها با یک وهلهی آن کار کرد. به همین جهت با this.route.parent در هر یک از Child Components تعریف شده کار میکنیم تا بتوان به یک وهلهی شیء محصول، دسترسی یافت.
د) همچنین میتوان از روش this.route.parent.data.subscribe نیز استفاده کرد. البته در اینجا چون صفحهی افزودن محصولات با صفحهی ویرایش محصولات، دارای root URL Segment یکسانی است، نیاز است از این روش استفاده کرد تا بتوان از تغییرات بعدی پارامتر id آن مطلع شد. این مورد روشی است که در کدهای ProductEditInfoComponent مشاهده میکنید.
ngOnInit(): void { this.route.parent.data.subscribe(data => { this.product = data['product']; if (this.productForm) { this.productForm.reset(); } }); }
شبیه به همین روش را در ProductEditTagsComponent نیز بکار گرفتهایم و در آنجا نیز با شیء this.route.parent و دسترسی به اطلاعات دریافتی از Route Resolver، کار میکنیم. به این ترتیب مطمئن خواهیم شد که this.product این دو کامپوننت مختلف، هر دو به یک وهله از شیء product دریافتی از سرور، اشاره میکنند.
به این ترتیب دکمهی Save ذیل هر دو برگه، به درستی عمل کرده و میتواند اطلاعات نهایی یک شیء محصول را ذخیره کند.
رفع مشکلات اعتبارسنجی فرمهای قرار گرفتهی در برگههای مختلف
علت استفادهی از ViewChild در ProductEditInfoComponent
@ViewChild(NgForm) productForm: NgForm;
<form class="form-horizontal" novalidate #productForm="ngForm">
انجام اینکار برای برگههای دوم به بعد ضروری نیست. از این جهت که با اولین بار نمایش این صفحه، تمام آنها از حافظه خارج میشوند و مجددا بازیابی خواهند شد.
مشکل دوم اعتبارسنجی این فرم چند برگهای این است که هرچند خالی کردن نام یا کد محصول، سبب نمایش خطای اعتبارسنجی میشود، اما سبب غیرفعال شدن دکمهی Save نخواهند شد؛ از این جهت که این دکمه در قالب والد قرار دارد و نه در قالب فرزندان.
در اولین بار نمایش Child Routes، کامپوننت ویرایش اطلاعات در router-outlet آن نمایش داده میشود. در این حالت اگر کاربر بر روی لینک نمایش کامپوننت edit tags کلیک کند، قالب کامپوننت edit info به طور کامل از router-outlet حذف میشود و با قالب کامپوننت edit tags جایگزین میشود. این فرآیند به این معنا است که فرم edit info به همراه تمام اطلاعات اعتبارسنجی آن unload میشوند. به همین ترتیب زمانیکه کاربر درخواست نمایش برگهی ویرایش اطلاعات را میکند، قالب edit tags و اطلاعات اعتبارسنجی آن unload میشوند. به این معنا که در یک router-outlet در هر زمان تنها یک فرم، به همراه اطلاعات اعتبارسنجی آن در دسترس هستند.
راه حلهای ممکن:
الف) بدنهی اصلی فرم را در کامپوننت والد قرار دهیم و سپس هر کدام از فرزندان، المانهای فرمهای مرتبط را ارائه دهند. این روش کار نمیکند چون Angular المانهای فرمهای قرار گرفتهی درون router-outlet را شناسایی نمیکند.
ب) قرار دادن فرمها، به صورت مجزا در هر کامپوننت فرزند (مانند روش فعلی) و سپس اعتبارسنجی دستی در کامپوننت والد.
تغییرات مورد نیاز کامپوننت ProductEditComponent را جهت افزودن اعتبارسنجی فرمهای فرزند آنرا در اینجا ملاحظه میکنید:
export class ProductEditComponent implements OnInit { private dataIsValid: { [key: string]: boolean } = {}; isValid(path: string): boolean { this.validate(); if (path) { return this.dataIsValid[path]; } return (this.dataIsValid && Object.keys(this.dataIsValid).every(d => this.dataIsValid[d] === true)); } saveProduct(): void { if (this.isValid(null)) { 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.'; } } validate(): void { // Clear the validation object this.dataIsValid = {}; // 'info' tab if (this.product.productName && this.product.productName.length >= 3 && this.product.productCode) { this.dataIsValid['info'] = true; } else { this.dataIsValid['info'] = false; } // 'tags' tab if (this.product.category && this.product.category.length >= 3) { this.dataIsValid['tags'] = true; } else { this.dataIsValid['tags'] = false; } } }
- سپس متد validate اضافه شدهاست تا کار اعتبارسنجی را انجام دهد. در اینجا از خود شیء this.product که بین دو برگه به اشتراک گذاشته شدهاست برای انجام اعتبارسنجی استفاده میکنیم. از این جهت که برگهها نیز با استفاده از this.route.parent.data، دقیقا به همین وهله دسترسی دارند. بنابراین هرتغییری که در برگهها بر روی این وهله اعمال شود، به کامپوننت والد نیز منعکس میشود.
- متد isValid، مسیر هر برگه را دریافت میکند و سپس به متغیر dataIsValid مراجعه کرده و وضعیت آن برگه را باز میگرداند. اگر path در اینجا قید نشود، وضعیت تمام برگهها بررسی میشوند؛ مانند if (this.isValid(null)) در متد ذخیره سازی اطلاعات.
- در آخر در فایل product-edit.component.html، وضعیت فعال و غیرفعال دکمهی ثبت را نیز به این متد متصل میکنیم:
<button class="btn btn-primary" type="button" style="width:80px;margin-right:10px" [disabled]="!isValid()" (click)="saveProduct()"> Save </button>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-04.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
یک سناریوی فرضی را در نظر بگیرید. اگر بخواهیم IdentityDbContext و دیگر DbContextهای اپلیکیشن را ادغام کنیم چه باید کرد؟ مثلا یک سیستم وبلاگ که برخی کاربران میتوانند پست جدید ثبت کنند، برخی تنها میتوانند کامنت بگذارند و تمامی کاربران هم اختیارات مشخص دیگری دارند. در چنین سیستمی شناسه کاربران (User ID) در بسیاری از مدلها (موجودیتها و مدلهای اپلیکیشن) وجود خواهد داشت تا مشخص شود هر رکورد به کدام کاربر متعلق است. در این مقاله چنین سناریو هایی را بررسی میکنیم و best practiceهای مربوطه را مرور میکنیم.
پس از ورود به سایت بعنوان یک مدیر، میتوانید ToDoهای ثبت شده توسط تمام کاربران را مشاهده کنید.
در این پست یک اپلیکیشن ساده ToDo خواهیم ساخت که امکان تخصیص to-doها به کاربران را نیز فراهم میکند. در این مثال خواهیم دید که چگونه میتوان مدلهای مختص به سیستم عضویت (IdentityDbContext) را با مدلهای دیگر اپلیکیشن مخلوط و استفاده کنیم.
تعریف نیازمندیهای اپلیکیشن
- تنها کاربران احراز هویت شده قادر خواهند بود تا لیست ToDoهای خود را ببینند، آیتمهای جدید ثبت کنند یا دادههای قبلی را ویرایش و حذف کنند.
- کاربران نباید آیتمهای ایجاد شده توسط دیگر کاربران را ببینند.
- تنها کاربرانی که به نقش Admin تعلق دارند باید بتوانند تمام ToDoهای ایجاد شده را ببینند.
پس بگذارید ببینیم چگونه میشود اپلیکیشنی با ASP.NET Identity ساخت که پاسخگوی این نیازمندیها باشد.
ابتدا یک پروژه ASP.NET MVC جدید با مدل احراز هویت Individual User Accounts بسازید. در این اپلیکیشن کاربران قادر خواهند بود تا بصورت محلی در وب سایت ثبت نام کنند و یا با تامین کنندگان دیگری مانند گوگل و فیسبوک وارد سایت شوند. برای ساده نگاه داشتن این پست ما از حسابهای کاربری محلی استفاده میکنیم.
در مرحله بعد ASP.NET Identity را راه اندازی کنید تا بتوانیم نقش مدیر و یک کاربر جدید بسازیم. میتوانید با اجرای اپلیکیشن راه اندازی اولیه را انجام دهید. از آنجا که سیستم ASP.NET Identity توسط Entity Framework مدیریت میشود میتوانید از تنظیمات پیکربندی Code First برای راه اندازی دیتابیس خود استفاده کنید.
در قدم بعدی راه انداز دیتابیس را در Global.asax تعریف کنید.
Database.SetInitializer<MyDbContext>(new MyDbInitializer());
ایجاد نقش مدیر و کاربر جدیدی که به این نقش تعلق دارد
اگر به قطعه کد زیر دقت کنید، میبینید که در خط شماره 5 متغیری از نوع UserManager ساخته ایم که امکان اجرای عملیات گوناگونی روی کاربران را فراهم میکند. مانند ایجاد، ویرایش، حذف و اعتبارسنجی کاربران. این کلاس که متعلق به سیستم ASP.NET Identity است همتای SQLMembershipProvider در ASP.NET 2.0 است.
در خط 6 یک RoleManager میسازیم که امکان کار با نقشها را فراهم میکند. این کلاس همتای SQLRoleMembershipProvider در ASP.NET 2.0 است.
در این مثال نام کلاس کاربران (موجودیت کاربر در IdentityDbContext) برابر با "MyUser" است، اما نام پیش فرض در قالبهای پروژه VS 2013 برابر با "ApplicationUser" میباشد.
public class MyDbInitializer : DropCreateDatabaseAlways<MyDbContext> { protected override void Seed(MyDbContext context) { var UserManager = new UserManager<MyUser>(new UserStore<MyUser>(context)); var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context)); string name = "Admin"; string password = "123456"; //Create Role Admin if it does not exist if (!RoleManager.RoleExists(name)) { var roleresult = RoleManager.Create(new IdentityRole(name)); } //Create User=Admin with password=123456 var user = new MyUser(); user.UserName = name; var adminresult = UserManager.Create(user, password); //Add User Admin to Role Admin if (adminresult.Succeeded) { var result = UserManager.AddToRole(user.Id, name); } base.Seed(context); } }
حال فایلی با نام Models/AppModels.cs بسازید و مدل EF Code First اپلیکیشن را تعریف کنید. از آنجا که از EF استفاده میکنیم، روابط کلیدها بین کاربران و ToDoها بصورت خودکار برقرار میشود.
public class MyUser : IdentityUser { public string HomeTown { get; set; } public virtual ICollection<ToDo> ToDoes { get; set; } } public class ToDo { public int Id { get; set; } public string Description { get; set; } public bool IsDone { get; set; } public virtual MyUser User { get; set; } }
در قدم بعدی با استفاده از مکانیزم Scaffolding کنترلر جدیدی بهمراه تمام Viewها و متدهای لازم برای عملیات CRUD بسازید. برای اطلاعات بیشتر درباره نحوه استفاده از مکانیزم Scaffolding به این لینک مراجعه کنید.
لطفا دقت کنید که از DbContext فعلی استفاده کنید. این کار مدیریت دادههای Identity و اپلیکیشن شما را یکپارچهتر میکند. DbContext شما باید چیزی شبیه به کد زیر باشد.
public class MyDbContext : IdentityDbContext<MyUser> { public MyDbContext() : base("DefaultConnection") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { public System.Data.Entity.DbSet<AspnetIdentitySample.Models.ToDo> ToDoes { get; set; } }
تنها کاربران احراز هویت شده باید قادر به اجرای عملیات CRUD باشند
برای این مورد از خاصیت Authorize استفاده خواهیم کرد که در MVC 4 هم وجود داشت. برای اطلاعات بیشتر لطفا به این لینک مراجعه کنید.
[Authorize] public class ToDoController : Controller
کنترلر ایجاد شده را ویرایش کنید تا کاربران را به ToDoها اختصاص دهد. در این مثال تنها اکشن متدهای Create و List را بررسی خواهیم کرد. با دنبال کردن همین روش میتوانید متدهای Edit و Delete را هم بسادگی تکمیل کنید.
یک متد constructor جدید بنویسید که آبجکتی از نوع UserManager میپذیرد. با استفاده از این کلاس میتوانید کاربران را در ASP.NET Identity مدیریت کنید.
private MyDbContext db; private UserManager<MyUser> manager; public ToDoController() { db = new MyDbContext(); manager = new UserManager<MyUser>(new UserStore<MyUser>(db)); }
اکشن متد Create را بروز رسانی کنید
هنگامی که یک ToDo جدید ایجاد میکنید، کاربر جاری را در ASP.NET Identity پیدا میکنیم و او را به ToDoها اختصاص میدهیم.
public async Task<ActionResult> Create ([Bind(Include="Id,Description,IsDone")] ToDo todo) { var currentUser = await manager.FindByIdAsync (User.Identity.GetUserId()); if (ModelState.IsValid) { todo.User = currentUser; db.ToDoes.Add(todo); await db.SaveChangesAsync(); return RedirectToAction("Index"); } return View(todo); }
اکشن متد List را بروز رسانی کنید
در این متد تنها ToDoهای کاربر جاری را باید بگیریم.
public ActionResult Index() { var currentUser = manager.FindById(User.Identity.GetUserId()); return View(db.ToDoes.ToList().Where( todo => todo.User.Id == currentUser.Id)); }
تنها مدیران سایت باید بتوانند تمام ToDoها را ببینند
بدین منظور ما یک اکشن متد جدید به کنترل مربوطه اضافه میکنیم که تمام ToDoها را لیست میکند. اما دسترسی به این متد را تنها برای کاربرانی که در نقش مدیر وجود دارند میسر میکنیم.
[Authorize(Roles="Admin")] public async Task<ActionResult> All() { return View(await db.ToDoes.ToListAsync()); }
نمایش جزئیات کاربران از جدول ToDo ها
از آنجا که ما کاربران را به ToDo هایشان مرتبط میکنیم، دسترسی به دادههای کاربر ساده است. مثلا در متدی که مدیر سایت تمام آیتمها را لیست میکند میتوانیم به اطلاعات پروفایل تک تک کاربران دسترسی داشته باشیم و آنها را در نمای خود بگنجانیم. در این مثال تنها یک فیلد بنام HomeTown اضافه شده است، که آن را در کنار اطلاعات ToDo نمایش میدهیم.
@model IEnumerable<AspnetIdentitySample.Models.ToDo> @{ ViewBag.Title = "Index"; } <h2>List of ToDoes for all Users</h2> <p> Notice that we can see the User info (UserName) and profile info such as HomeTown for the user as well. This was possible because we associated the User object with a ToDo object and hence we can get this rich behavior. 12: </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.IsDone) </th> <th>@Html.DisplayNameFor(model => model.User.UserName)</th> <th>@Html.DisplayNameFor(model => model.User.HomeTown)</th> </tr> 25: 26: @foreach (var item in Model) 27: { 28: <tr> 29: <td> 30: @Html.DisplayFor(modelItem => item.Description) 31: </td> 32: <td> @Html.DisplayFor(modelItem => item.IsDone) </td> <td> @Html.DisplayFor(modelItem => item.User.UserName) </td> <td> @Html.DisplayFor(modelItem => item.User.HomeTown) </td> </tr> } </table>
صفحه Layout را بروز رسانی کنید تا به ToDoها لینک شود
<li>@Html.ActionLink("ToDo", "Index", "ToDo")</li> <li>@Html.ActionLink("ToDo for User In Role Admin", "All", "ToDo")</li>
حال اپلیکیشن را اجرا کنید. همانطور که مشاهده میکنید دو لینک جدید به منوی سایت اضافه شده اند.
ساخت یک ToDo بعنوان کاربر عادی
روی لینک ToDo کلیک کنید، باید به صفحه ورود هدایت شوید چرا که دسترسی تنها برای کاربران احراز هویت شده تعریف وجود دارد. میتوانید یک حساب کاربری محلی ساخته، با آن وارد سایت شوید و یک ToDo بسازید.
پس از ساختن یک ToDo میتوانید لیست رکوردهای خود را مشاهده کنید. دقت داشته باشید که رکوردهایی که کاربران دیگر ثبت کرده اند برای شما نمایش داده نخواهند شد.
مشاهده تمام ToDoها بعنوان مدیر سایت
روی لینک ToDoes for User in Role Admin کلیک کنید. در این مرحله باید مجددا به صفحه ورود هدایت شوید چرا که شما در نقش مدیر نیستید و دسترسی کافی برای مشاهده صفحه مورد نظر را ندارید. از سایت خارج شوید و توسط حساب کاربری مدیری که هنگام راه اندازی اولیه دیتابیس ساخته اید وارد سایت شوید.
User = Admin Password = 123456
مطالب
EF Code First #9
تنظیمات ارث بری کلاسها در EF Code first
بانکهای اطلاعاتی مبتنی بر SQL، تنها روابطی از نوع «has a» یا «دارای» را پشتیبانی میکنند؛ اما در دنیای شیءگرا روابطی مانند «is a» یا «هست» نیز قابل تعریف هستند. برای توضیحات بیشتر به مدلهای زیر دقت نمائید:
using System;
namespace EF_Sample05.DomainClasses.Models
{
public abstract class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
}
}
namespace EF_Sample05.DomainClasses.Models
{
public class Coach : Person
{
public string TeamName { set; get; }
}
}
namespace EF_Sample05.DomainClasses.Models
{
public class Player : Person
{
public int Number { get; set; }
public string Description { get; set; }
}
}
در این مدلها که بر اساس ارث بری از کلاس شخص، تهیه شدهاند؛ بازیکن، یک شخص است. مربی نیز یک شخص است؛ و به این ترتیب خوانده میشوند:
Coach "is a" Person
Player "is a" Person
در EF Code first سه روش جهت کار با این نوع کلاسها و کلا ارث بری وجود دارد که در ادامه به آنها خواهیم پرداخت:
الف) Table per Hierarchy یا TPH
همانطور که از نام آن نیز پیدا است، کل سلسله مراتبی را که توسط ارث بری تعریف شده است، تبدیل به یک جدول در بانک اطلاعاتی میکند. این حالت، شیوه برخورد پیش فرض EF Code first با ارث بری کلاسها است و نیاز به هیچگونه تنظیم خاصی ندارد.
برای آزمایش این مساله، کلاس Context را به نحو زیر تعریف نمائید و سپس اجازه دهید تا EF بانک اطلاعاتی معادل آنرا تولید کند:
using System.Data.Entity;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Context
{
public class Sample05Context : DbContext
{
public DbSet<Person> People { set; get; }
}
}
ساختار جدول تولید شده آن همانند تصویر زیر است:
همانطور که ملاحظه میکنید، تمام کلاسهای مشتق شده از کلاس شخص را تبدیل به یک جدول کرده است؛ به علاوه یک فیلد جدید را هم به نام Discriminator به این جدول اضافه نموده است. برای درک بهتر عملکرد این فیلد، چند رکورد را توسط برنامه به بانک اطلاعاتی اضافه میکنیم. حاصل آن به شکل زیر خواهد بود:
از فیلد Discriminator جهت ثبت نام کلاسهای متناظر با هر رکورد، استفاده شده است. به این ترتیب EF حین کار با اشیاء دقیقا میداند که چگونه باید خواص متناظر با کلاسهای مختلف را مقدار دهی کند.
به علاوه اگر به ساختار جدول تهیه شده دقت کنید، مشخص است که در حالت TPH، نیاز است فیلدهای متناظر با کلاسهای مشتق شده از کلاس پایه، همگی null پذیر باشند. برای نمونه فیلد Number که از نوع int تعریف شده، در سمت بانک اطلاعاتی نال پذیر تعریف شده است.
و برای کوئری نوشتن در این حالت میتوان از متد الحاقی OfType جهت فیلتر کردن اطلاعات بر اساس کلاسی خاص، کمک گرفت:
db.People.OfType<Coach>().FirstOrDefault(x => x.LastName == "Coach L1")
سفارشی سازی نحوه نگاشت TPH
همانطور که عنوان شد، TPH نیاز به تنظیمات خاصی ندارد و حالت پیش فرض است؛ اما برای مثال میتوان بر روی مقادیر و نوع ستون Discriminator تولیدی، کنترل داشت. برای این منظور باید از Fluent API به نحو زیر استفاده کرد:
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class CoachConfig : EntityTypeConfiguration<Coach>
{
public CoachConfig()
{
// For TPH
this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PlayerConfig : EntityTypeConfiguration<Player>
{
public PlayerConfig()
{
// For TPH
this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));
}
}
}
در اینجا توسط متد Map، نام فیلد discriminator به PersonType تغییر کرده. همچنین چون مقدار پیش فرض تعیین شده توسط متد HasValue عددی است، نوع این فیلد در سمت بانک اطلاعاتی به int null تغییر میکند.
ب) Table per Type یا TPT
در حالت TPT، به ازای هر کلاس موجود در سلسله مراتب تعیین شده، یک جدول در سمت بانک اطلاعاتی تشکیل میگردد.
در جداول متناظر با Sub classes، تنها همان فیلدهایی وجود خواهند داشت که در کلاسهای هم نام وجود دارد و فیلدهای کلاس پایه در آنها ذکر نخواهد گردید. همچنین این جداول دارای یک Primary key نیز خواهند بود (که دقیقا همان کلید اصلی جدول پایه است که به آن Shared primary key هم گفته میشود). این کلید اصلی، به عنوان کلید خارجی اشاره کننده به کلاس یا جدول پایه نیز تنظیم میگردد:
برای تنظیم این نوع ارث بری، تنها کافی است ویژگی Table را بر روی Sub classes قرار داد:
using System.ComponentModel.DataAnnotations;
namespace EF_Sample05.DomainClasses.Models
{
[Table("Coaches")]
public class Coach : Person
{
public string TeamName { set; get; }
}
}
using System.ComponentModel.DataAnnotations;
namespace EF_Sample05.DomainClasses.Models
{
[Table("Players")]
public class Player : Person
{
public int Number { get; set; }
public string Description { get; set; }
}
}
یا اگر حالت Fluent API را ترجیح میدهید، همانطور که در قسمتهای قبل نیز ذکر شد، معادل ویژگی Table در اینجا، متد ToTable است.
ج) Table per Concrete type یا TPC
در تعاریف ارث بری که تاکنون بررسی کردیم، مرسوم است کلاس پایه را از نوع abstract تعریف کنند. به این ترتیب هدف اصلی، Sub classes تعریف شده خواهند بود؛ چون نمیتوان مستقیما وهلهای را از کلاس abstract تعریف شده ایجاد کرد.
در حالت TPC، به ازای هر sub class غیر abstract، یک جدول ایجاد میشود. هر جدول نیز حاوی فیلدهای کلاس پایه میباشد (برخلاف حالت TPT که جداول متناظر با کلاسهای مشتق شده، تنها حاوی همان خواص و فیلدهای کلاسهای متناظر بودند و نه بیشتر). به این ترتیب عملا جداول تشکیل شده در بانک اطلاعاتی، از وجود ارث بری در سمت کدهای ما بیخبر خواهند بود.
برای پیاده سازی TPC نیاز است از Fluent API استفاده شود:
using System.ComponentModel.DataAnnotations;
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PersonConfig : EntityTypeConfiguration<Person>
{
public PersonConfig()
{
// for TPC
this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None);
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class CoachConfig : EntityTypeConfiguration<Coach>
{
public CoachConfig()
{
// For TPH
//this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1));
// for TPT
//this.ToTable("Coaches");
//for TPC
this.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Coaches");
});
}
}
}
using System.Data.Entity.ModelConfiguration;
using EF_Sample05.DomainClasses.Models;
namespace EF_Sample05.DataLayer.Mappings
{
public class PlayerConfig : EntityTypeConfiguration<Player>
{
public PlayerConfig()
{
// For TPH
//this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2));
// for TPT
//this.ToTable("Players");
//for TPC
this.Map(m =>
{
m.MapInheritedProperties();
m.ToTable("Players");
});
}
}
}
ابتدا نوع فیلد Id از حالت Identity خارج شده است. این مورد جهت کار با TPC ضروری است در غیراینصورت EF هنگام ثبت، به مشکل بر میخورد، از این لحاظ که برای دو شیء، به یک Id خواهد رسید و امکان ثبت را نخواهد داد. بنابراین در یک چنین حالتی استفاده از نوع Guid برای تعریف primary key شاید بهتر باشد. بدیهی است در این حالت باید Id را به صورت دستی مقدار دهی نمود.
در ادامه توسط متد MapInheritedProperties، به همان مقصود لحاظ کردن تمام فیلدهای ارث بری شده در جدول حاصل، خواهیم رسید. همچنین نام جداول متناظر نیز ذکر گردیده است.
سؤال : از این بین، بهتر است از کدامیک استفاده شود؟
- برای حالتهای ساده از TPH استفاده کنید. برای مثال یک بانک اطلاعاتی قدیمی دارید که هر جدول آن 200 تا یا شاید بیشتر فیلد دارد! امکان تغییر طراحی آن هم وجود ندارد. برای اینکه بتوان به حس بهتری حین کارکردن با این نوع سیستمهای قدیمی رسید، میشود از ترکیب TPH و ComplexTypes (که در قسمتهای قبل در مورد آن بحث شد) برای مدیریت بهتر این نوع جداول در سمت کدهای برنامه استفاده کرد.
- اگر علاقمند به استفاده از روابط پلیمرفیک هستید ( برای مثال در کلاسی دیگر، ارجاعی به کلاس پایه Person وجود دارد) و sub classes دارای تعداد فیلدهای کمی هستند، از TPH استفاده کنید.
- اگر تعداد فیلدهای sub classes زیاد است و بسیار بیشتر است از کلاس پایه، از روش TPT استفاده کنید.
- اگر عمق ارث بری و تعداد سطوح تعریف شده بالا است، بهتر است از TPC استفاده کنید. حالت TPT از join استفاده میکند و حالت TPC از union برای تشکیل کوئریها کمک خواهد گرفت
در مطلب «تولید پویای ستونها در PdfReport» عنوان شد که ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. همچنین به کمک متد MainTableAdHocColumnsConventions میتوان بر اساس نوعهای دادهای، بر روی نحوه نمایش ستونها تاثیر گذاشت. برای مثال هرجایی DateTime مشاهده شد، به صورت خودکار تبدیل به تاریخ شمسی شود.
روش دیگری که این روزها در اکثر فریمهای دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیتهای تعریف شده کلاسها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده میکند (در تولید پویای کد، یا اعتبار سنجیهای سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدلهای اینگونه فریم ورکها، اکثر ویژگیها و Data Annotations متداول را نیز میتوان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آنها خواهیم پرداخت.
آشنایی با مدلهای بکار رفته در مثال جاری:
در اینجا یک enum، جهت تعیین سمت شغلی تعریف شده است. برای اینکه بتوان خروجی مطلوبی را در گزارشات شاهد بود، میتوان از ویژگی Description، جهت تعیین مقدار نمایشی آنها نیز استفاده کرد و این تعاریف در PdfReport خوانده و اعمال میشوند.
مدل فوق جهت مقدار دهی اطلاعات یک شخص تعریف شده است.
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسبهای سرستونها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالبهای ستونهای سفارشی و یا حتی قالبهای پیش فرض دیگری که متنی نیستند، وجود دارد، میتوانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاسهای پیش فرض قالبهای ستونها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا میتوان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونهای از استفاده از آنرا برای تعیین نحوه نمایش تاریخ، ملاحظه میکنید. توسط این ویژگی حتی میتوان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آنها نیز پیش بینی شده است که در قسمتهای بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه میکنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمیتوان توسط ویژگیها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)
در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
در پایان، نحوه استفاده از منبع داده فوق جهت تامین یک گزارش، به نحو زیر میباشد:
همانطور که مشخص است، از ذکر متد MainTableColumns به علت استفاده از DataAnnotations صرفنظر شده و PdfReport این تعاریف را بر اساس ویژگیهای خواص کلاس شخص دریافت میکند. تنها از متد MainTableAdHocColumnsConventions جهت مشخص سازی اینکه نیاز به نمایش ستون ردیف میباشد، استفاده کردهایم.
روش دیگری که این روزها در اکثر فریمهای دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیتهای تعریف شده کلاسها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده میکند (در تولید پویای کد، یا اعتبار سنجیهای سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدلهای اینگونه فریم ورکها، اکثر ویژگیها و Data Annotations متداول را نیز میتوان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آنها خواهیم پرداخت.
آشنایی با مدلهای بکار رفته در مثال جاری:
using System.ComponentModel; namespace PdfReportSamples.Models { public enum JobTitle { [Description("Grunt")] Grunt, [Description("Programmer")] Programmer, [Description("Analyst Programmer")] AnalystProgrammer, [Description("Project Manager")] ProjectManager, [Description("Chief Information Officer")] ChiefInformationOfficer, } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using PdfReportSamples.Models; using PdfRpt.Aggregates.Numbers; using PdfRpt.ColumnsItemsTemplates; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; using PdfRpt.DataAnnotations; namespace PdfReportSamples.DataAnnotations { public class Person { [IsVisible(false)] public int Id { get; set; } [DisplayName("User name")] //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically. [ColumnItemsTemplate(typeof(TextBlockField))] public string Name { get; set; } [DisplayName("Job title")] public JobTitle JobTitle { set; get; } [DisplayName("Date of birth")] [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")] public DateTime DateOfBirth { get; set; } [DisplayName("Date of death")] [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")] public DateTime? DateOfDeath { get; set; } [DisplayFormat(DataFormatString = "{0:n0}")] [CustomAggregateFunction(typeof(Sum))] public int Salary { get; set; } [IsCalculatedField(true)] [DisplayName("Calculated Field")] [DisplayFormat(DataFormatString = "{0:n0}")] [AggregateFunction(AggregateFunction.Sum)] public string CalculatedField { get; set; } [CalculatedFieldFormula("CalculatedField")] public static Func<IList<CellData>, object> CalculatedFieldFormula = list => { if (list == null) return string.Empty; var salary = (int)list.GetValueOf<Person>(x => x.Salary); return salary * 0.8; };//Note: It's a static field, not a property. } }
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسبهای سرستونها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالبهای ستونهای سفارشی و یا حتی قالبهای پیش فرض دیگری که متنی نیستند، وجود دارد، میتوانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاسهای پیش فرض قالبهای ستونها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا میتوان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونهای از استفاده از آنرا برای تعیین نحوه نمایش تاریخ، ملاحظه میکنید. توسط این ویژگی حتی میتوان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آنها نیز پیش بینی شده است که در قسمتهای بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه میکنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمیتوان توسط ویژگیها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)
در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
using System; using System.Collections.Generic; using PdfReportSamples.Models; namespace PdfReportSamples.DataAnnotations { public static class PersonnelDataSource { public static IList<Person> CreatePersonnelList() { return new List<Person> { new Person { Id = 1, Name = "Edward", DateOfBirth = new DateTime(1900, 1, 1), DateOfDeath = new DateTime(1990, 10, 15), JobTitle = JobTitle.ChiefInformationOfficer, Salary = 5000 }, new Person { Id = 2, Name = "Margaret", DateOfBirth = new DateTime(1950, 2, 9), DateOfDeath = null, JobTitle = JobTitle.AnalystProgrammer, Salary = 4000 }, new Person { Id = 3, Name = "Grant", DateOfBirth = new DateTime(1975, 6, 13), DateOfDeath = null, JobTitle = JobTitle.Programmer, Salary = 3500 } }; } } }
using System; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.DataAnnotations { public class DataAnnotationsPdfReport { public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("new rpt."); defaultHeader.RunDirection(PdfRunDirection.LeftToRight); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.FitToContent); }) .MainTableDataSource(dataSource => { dataSource.StronglyTypedList(PersonnelDataSource.CreatePersonnelList()); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .MainTableSummarySettings(summary => { summary.OverallSummarySettings("Total"); summary.PageSummarySettings("Page Summary"); summary.PreviousPageSummarySettings("Pervious Page Summary"); }) .MainTableAdHocColumnsConventions(adHocColumns => { adHocColumns.ShowRowNumberColumn(true); adHocColumns.RowNumberColumnCaption("#"); }) .Export(export => { export.ToExcel(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\DataAnnotationsSampleRpt.pdf")); } } }
همانطور که در مطلب «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» ملاحظه کردید، برای سازگار سازی یک فرم جدید ایجاد شده ASP.NET MVC با پیش فرضهای Twitter bootstrap، حداقل 8 مرحله باید طی شود و ... چقدر خوب میشد اگر اینکارها به صورت خودکار توسط VS.NET بجای قالب پیش فرض ایجاد فرم آن، تولید میشد. در ادامه قصد داریم این سفارشی سازی را انجام دهیم.
مراحل کلی سفارشی سازی قالبهای Scaffolding پیش فرض ASP.NET MVC
قالبهای Scaffolding پیش فرض ASP.NET در مسیر Microsoft Visual Studio X\Common7\IDE\ItemTemplates\CSharp\Web\MVC X\CodeTemplates قرار دارند. برای نمونه اگر بخواهیم پیش فرضهای تولید فرمهای MVC4 را تغییر دهیم، باید به پوشه MVC 4\CodeTemplates\AddView\CSHTML مراجعه و فایل Create.tt را ویرایش کنیم.
اینکار هرچند عملی است اما آنچنان جالب نیست؛ از این جهت که تاثیری کلی و سراسری خواهد داشت.
برای اعمال محلی این تغییرات فقط به یک پروژه خاص، تنها کافی است همین مسیر CodeTemplates\AddView\CSHTML به همراه تمام فایلهای tt آن، در پوشه جاری پروژه مدنظر ما کپی شود. به این ترتیب ابتدا به این پوشه محلی مراجعه خواهد شد.
روش دوم کپی کردن این فایلها، استفاده از بسته نیوگت ذیل است:
سفارشی سازی فایل Create.tt پیش فرض ASP.NET MVC جهت سازگار سازی آن با Twitter bootstrap
در اینجا قصد داریم همان 8 مرحله مطلب «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» را به فایل Create.tt که اکنون در پوشه CodeTemplates\AddView\CSHTML\Create.tt ریشه پروژه جاری قرار دارد، اعمال کنیم.
الف) ابتدا نام این فایل را به CreateBootstrapForm.tt تغییر میدهیم. از این لحاظ که این نام جدید در drop down مرتبط با scaffold template صفحه Add view ظاهر خواهد شد. به علاوه نیازی نیست تا این فایل tt در همان لحظه اجرا شود، بنابراین به خواص آن در VS.NET مراجعه کرده و مقدار گزینه custom tool آنرا خالی میکنیم (مانند سایر فایلهای tt اضافه شده).
ب) قسمت ابتدایی فایل CreateBootstrapForm.tt را که همان کپی مطابق اصل فایل Create.tt است، به نحو ذیل تغییر میدهیم:
که حاصل آن به صورت ذیل قابل استفاده و دسترسی خواهد بود:
دریافت فایل CreateBootstrapForm.tt اصلاح شده:
همانطور که عنوان شد، برای استفاده از آن فقط کافی است آنرا در مسیر CodeTemplates\AddView\CSHTML\CreateBootstrapForm.tt ریشه پروژه جاری خود کپی کنید.
مراحل کلی سفارشی سازی قالبهای Scaffolding پیش فرض ASP.NET MVC
قالبهای Scaffolding پیش فرض ASP.NET در مسیر Microsoft Visual Studio X\Common7\IDE\ItemTemplates\CSharp\Web\MVC X\CodeTemplates قرار دارند. برای نمونه اگر بخواهیم پیش فرضهای تولید فرمهای MVC4 را تغییر دهیم، باید به پوشه MVC 4\CodeTemplates\AddView\CSHTML مراجعه و فایل Create.tt را ویرایش کنیم.
اینکار هرچند عملی است اما آنچنان جالب نیست؛ از این جهت که تاثیری کلی و سراسری خواهد داشت.
برای اعمال محلی این تغییرات فقط به یک پروژه خاص، تنها کافی است همین مسیر CodeTemplates\AddView\CSHTML به همراه تمام فایلهای tt آن، در پوشه جاری پروژه مدنظر ما کپی شود. به این ترتیب ابتدا به این پوشه محلی مراجعه خواهد شد.
روش دوم کپی کردن این فایلها، استفاده از بسته نیوگت ذیل است:
PM> Install-Package Mvc4CodeTemplatesCSharp
سفارشی سازی فایل Create.tt پیش فرض ASP.NET MVC جهت سازگار سازی آن با Twitter bootstrap
در اینجا قصد داریم همان 8 مرحله مطلب «اعمال کلاسهای ویژه اعتبارسنجی Twitter bootstrap به فرمهای ASP.NET MVC» را به فایل Create.tt که اکنون در پوشه CodeTemplates\AddView\CSHTML\Create.tt ریشه پروژه جاری قرار دارد، اعمال کنیم.
الف) ابتدا نام این فایل را به CreateBootstrapForm.tt تغییر میدهیم. از این لحاظ که این نام جدید در drop down مرتبط با scaffold template صفحه Add view ظاهر خواهد شد. به علاوه نیازی نیست تا این فایل tt در همان لحظه اجرا شود، بنابراین به خواص آن در VS.NET مراجعه کرده و مقدار گزینه custom tool آنرا خالی میکنیم (مانند سایر فایلهای tt اضافه شده).
ب) قسمت ابتدایی فایل CreateBootstrapForm.tt را که همان کپی مطابق اصل فایل Create.tt است، به نحو ذیل تغییر میدهیم:
<# if (!mvcHost.IsContentPage) { #> <script src="~/Scripts/jquery-1.9.1.min.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <# } } #> @using (Html.BeginForm()) { @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) <fieldset class="form-horizontal"> <legend><#= mvcHost.ViewDataType.Name #></legend> <# foreach (ModelProperty property in GetModelProperties(mvcHost.ViewDataType)) { if (!property.IsPrimaryKey && !property.IsReadOnly && property.Scaffold) { #> <div class="control-group"> <# if (property.IsForeignKey) { #> @Html.LabelFor(model => model.<#= property.Name #>, "<#= property.AssociationName #>",new {@class="control-label"}) <# } else { #> @Html.LabelFor(model => model.<#= property.Name #>,new {@class="control-label"}) <# } #> <div class="controls"> <# if (property.IsForeignKey) { #> @Html.DropDownList("<#= property.Name #>", String.Empty) <# } else { #> @Html.EditorFor(model => model.<#= property.Name #>) <# } #> @Html.ValidationMessageFor(model => model.<#= property.Name #>,null,new{@class="help-inline"}) </div> </div> <# } } #> <div class="form-actions"> <button type="submit" class="btn btn-primary">ارسال</button> <button class="btn">لغو</button> </div> </fieldset> } <div> @Html.ActionLink("Back to List", "Index") </div> <# if(mvcHost.IsContentPage && mvcHost.ReferenceScriptLibraries) { #> @section JavaScript { }
دریافت فایل CreateBootstrapForm.tt اصلاح شده:
همانطور که عنوان شد، برای استفاده از آن فقط کافی است آنرا در مسیر CodeTemplates\AddView\CSHTML\CreateBootstrapForm.tt ریشه پروژه جاری خود کپی کنید.
نظرات مطالب
پیاده سازی Option یا Maybe در #C
در اولین کد نوشته اید:
اگر user==null باشد خطای HttpNotFound و در غیراینصورت ویوی متناظر برگشت داده شود.
در ادامه "نوشتن if اضافه جهت چک کردن نال رفرنس" را بعنوان ایراد مطرح کردید.
اما در دومین کد، رفتار متد تغییر کرد تا if حذف شود (در هر صورت یک ویو برگشت داده میشود).
بجای
نوشتید:
اگر هدف متد؛ انجام کار به شکل کد دوم باشد میتوان همان متد اولیه را بدون استفاده از Maybe و بصورت تک خطی نوشت:
هم کدها ساده شده اند و هم if و خیلی چیزهای دیگه حذف شده اند.
بهرحال در نسخه نهایی بررسی نال رفرنس همچنان وجود دارد و تنها خودش رو به شکل دیگری بروز میدهد: DefaultIfEmpty(new User()).
آیا با این روش فقط کدها را پیچیدهتر نمیکنیم؟ (نقض KISS )
آیا میشود همان منطق اولیه را با این روش انجام داد؟ (بدون پاسکاری یا موکول کردن آن به متد دیگر)
آیا در کل لزومی به استفاده از Maybe یا Option هست؟
...
اگر user==null باشد خطای HttpNotFound و در غیراینصورت ویوی متناظر برگشت داده شود.
در ادامه "نوشتن if اضافه جهت چک کردن نال رفرنس" را بعنوان ایراد مطرح کردید.
اما در دومین کد، رفتار متد تغییر کرد تا if حذف شود (در هر صورت یک ویو برگشت داده میشود).
بجای
public ActionResult Details(int id) { var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند if( user == null) return HttpNotFound(); return View(user); }
public ActionResult Details(int id) { var user = _userService .GetById(3) .DefaultIfEmpty(new User()) .Single(); return View(user); }
اگر هدف متد؛ انجام کار به شکل کد دوم باشد میتوان همان متد اولیه را بدون استفاده از Maybe و بصورت تک خطی نوشت:
public ActionResult Details(int id) { return View(_userService.GetById(3) ?? new User()); }
بهرحال در نسخه نهایی بررسی نال رفرنس همچنان وجود دارد و تنها خودش رو به شکل دیگری بروز میدهد: DefaultIfEmpty(new User()).
آیا با این روش فقط کدها را پیچیدهتر نمیکنیم؟ (نقض KISS )
آیا میشود همان منطق اولیه را با این روش انجام داد؟ (بدون پاسکاری یا موکول کردن آن به متد دیگر)
public ActionResult Details(int id) { var user = _userService.GetById(3); return user == null ? HttpNotFound() : View(user); }
آیا در کل لزومی به استفاده از Maybe یا Option هست؟
...
اجرای Async اعمال نسبتا طولانی، در برنامههای مبتنی بر داده، عموما این مزایا را به همراه دارد:
الف) مقیاس پذیری سمت سرور
در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواستهای کلاینتها تدارک دیده میشوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام میدهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواستهایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه میگردد. یکی از روشهای رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونههای non-blocking است. به این ترتیب ترد پردازش کنندهی این اعمال Async بلافاصله آزاد شده و سرور میتواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.
ب) بالا بردن پاسخ دهی کلاینتها
کلاینتها نیز اگر مدام درخواستهای blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز میتوان از توانمندیهای Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.
و ... تمام اینها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی میکنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانکهای اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کردهاست که در ادامه جزئیات آنرا بررسی خواهیم کرد.
پیشنیازها
برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژههای کلیدی async و await را پشتیبانی میکند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی میکند، دات نت 4 در اینجا قابل استفاده نخواهد بود.
متدهای الحاقی جدید Async در EF 6.x
جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونههای Async آنها نیز اضافه شدهاند:
بنابراین اولین قدم تبدیل کدهای قدیمی به Async، استفاده از متدهای الحاقی فوق است.
چند مثال
فرض کنید، مدلهای برنامه، رابطهی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
همچنین Context برنامه نیز جهت در معرض دید قرار دادن این کلاسها، به نحو ذیل تشکیل شدهاست:
بر این اساس مثالی که دو رکورد را در جداول کاربران و مقالات به صورت async ثبت میکند، به نحو ذیل خواهد بود:
چند نکته جهت یادآوری مباحث Async
- به امضای متد واژهی کلیدی async اضافه شدهاست، زیرا در بدنهی آن از کلمهی کلیدی await استفاده کردهایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمهی Async اضافه شدهاست. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شدهاست.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجیهای توابع نیاز است از نوع Task باشند و آرگومان جنریک آنها، بیانگر نوع مقداری که باز میگردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه میکنید.
نمونهای از نحوهی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر میتواند باشد:
در اینجا به امضای اکشن متد جاری، async اضافه شدهاست و خروجی آن نیز به نوع Task تغییر یافته است. همچنین یک پارامتر cancellationToken نیز تعریف شدهاست. این پارامتر به صورت خودکار توسط ASP.NET MVC پس از زمانیکه توسط ویژگی AsyncTimeout تعیین شدهاست، تنظیم خواهد شد. به این ترتیب، اعمال async در حال اجرا به صورت خودکار لغو میشوند.
- برای اجرا و دریافت نتیجهی متدهای Async دار EF، نیاز است از واژهی کلیدی await استفاده گردد.
استفاده کننده نیز میتواند متد addUserAsync را به صورت زیر فراخوانی کند:
شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
کدهای Async تقلبی!
به قطعه کد ذیل دقت کنید:
این متد از یکی از Generic repositoryهای فلهای رها شده در اینترنت انتخاب شدهاست.
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آنها استفاده میشود، کدهای Async تقلبی میگویند! این عملیات هر چند در یک ترد دیگر انجام میشود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده میکنید:
متد ToListAsync یک متد Async واقعی است و نه شبیه سازی شده توسط Task.Run. متدهای Async واقعی کار با شبکه و اعمال I/O، از ترد استفاده نمیکنند و توسط سیستم عامل به نحو بسیار بهینهای اجرا میگردند.
برای مثال پشت صحنهی متد الحاقی SaveChangesAsync به یک چنین متدی ختم میشود:
متد ExecuteNonQueryAsync جزو متدهای ADO.NET 4.5 است و برای اجرا نیاز به هیچ ترد جدیدی ندارد.
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است:
محدودیت پردازش موازی اعمال در EF
در متد ذیل، دو Task غیرهمزمان تعریف شدهاند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آنها را کردهایم:
این متد ممکن است اجرا شود؛ یا در بعضی از مواقع با استثنای ذیل خاتمه یابد:
متن استثنای ارائه شده بسیار مفید است و توضیحات کامل را به همراه دارد. در EF در طی یک Context اگر عملیات Async شروع شدهای خاتمه نیافته باشد، مجاز به شروع یک عملیات Async دیگر، به موازت آن نخواهیم بود. برای رفع این مشکل یا باید از چندین Context استفاده کرد و یا await Task.WhenAll را حذف کرده و بجای آن واژهی کلیدی await را همانند معمول، جهت صبر کردن برای دریافت نتیجهی یک عملیات غیرهمزمان استفاده کنیم.
الف) مقیاس پذیری سمت سرور
در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواستهای کلاینتها تدارک دیده میشوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام میدهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواستهایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه میگردد. یکی از روشهای رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونههای non-blocking است. به این ترتیب ترد پردازش کنندهی این اعمال Async بلافاصله آزاد شده و سرور میتواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.
ب) بالا بردن پاسخ دهی کلاینتها
کلاینتها نیز اگر مدام درخواستهای blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز میتوان از توانمندیهای Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.
و ... تمام اینها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی میکنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانکهای اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کردهاست که در ادامه جزئیات آنرا بررسی خواهیم کرد.
پیشنیازها
برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژههای کلیدی async و await را پشتیبانی میکند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی میکند، دات نت 4 در اینجا قابل استفاده نخواهد بود.
متدهای الحاقی جدید Async در EF 6.x
جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونههای Async آنها نیز اضافه شدهاند:
QueryableExtensions: AllAsync AnyAsync AverageAsync ContainsAsync CountAsync FirstAsync FirstOrDefaultAsync ForEachAsync LoadAsync LongCountAsync MaxAsync MinAsync SingleAsync SingleOrDefaultAsync SumAsync ToArrayAsync ToDictionaryAsync ToListAsync DbSet: FindAsync DbContext: SaveChangesAsync Database: ExecuteSqlCommandAsync
چند مثال
فرض کنید، مدلهای برنامه، رابطهی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } } public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } }
public class MyContext : DbContext { public DbSet<User> Users { get; set; } public DbSet<BlogPost> BlogPosts { get; set; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } }
private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user = context.Users.Add(new User { Name = "Vahid" }); context.BlogPosts.Add(new BlogPost { Content = "Test", Title = "Test", User = user }); await context.SaveChangesAsync(cancellationToken); return user; } }
چند نکته جهت یادآوری مباحث Async
- به امضای متد واژهی کلیدی async اضافه شدهاست، زیرا در بدنهی آن از کلمهی کلیدی await استفاده کردهایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمهی Async اضافه شدهاست. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شدهاست.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجیهای توابع نیاز است از نوع Task باشند و آرگومان جنریک آنها، بیانگر نوع مقداری که باز میگردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه میکنید.
نمونهای از نحوهی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر میتواند باشد:
[AsyncTimeout(8000)] public async Task<ActionResult> Index(CancellationToken cancellationToken)
- برای اجرا و دریافت نتیجهی متدهای Async دار EF، نیاز است از واژهی کلیدی await استفاده گردد.
استفاده کننده نیز میتواند متد addUserAsync را به صورت زیر فراخوانی کند:
var user = await addUserAsync(); Console.WriteLine("user id: {0}", user.Id);
شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) user1.Name = "Vahid N."; await context.SaveChangesAsync(cancellationToken); return user1; } } private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var user1 = await context.Users.FindAsync(cancellationToken, 1); if (user1 != null) context.Users.Remove(user1); return await context.SaveChangesAsync(cancellationToken); } }
کدهای Async تقلبی!
به قطعه کد ذیل دقت کنید:
public async Task<List<TEntity>> GetAllAsync() { return await Task.Run(() => _tEntities.ToList()); }
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آنها استفاده میشود، کدهای Async تقلبی میگویند! این عملیات هر چند در یک ترد دیگر انجام میشود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده میکنید:
private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { return await context.Users.ToListAsync(cancellationToken); } }
برای مثال پشت صحنهی متد الحاقی SaveChangesAsync به یک چنین متدی ختم میشود:
internal override async Task<long> ExecuteAsync( //... rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false); //...
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است:
var connectionString = "........"; var sql = @"......""; var users = new List<User>(); using (var cnx = new SqlConnection(connectionString)) { using (var cmd = new SqlCommand(sql, cnx)) { await cnx.OpenAsync(); using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection)) { while (await reader.ReadAsync()) { var user = new User { Id = reader.GetInt32(0), Name = reader.GetString(1), }; users.Add(user); } } } }
محدودیت پردازش موازی اعمال در EF
در متد ذیل، دو Task غیرهمزمان تعریف شدهاند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آنها را کردهایم:
// multiple operations private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken)) { using (var context = new MyContext()) { var task1 = context.Users.ToListAsync(cancellationToken); var task2 = context.BlogPosts.ToListAsync(cancellationToken); await Task.WhenAll(task1, task2); // use task1.Result } }
An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll Additional information: A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.