I can tell at least that in 3 years, JavaScript will gain more the status of a VM and lose the status of a language. Already today, not many people use raw JavaScript. You usually have some transpilation, at least e.g. Babel. In the future, Web Assembly will enable more innovation in that regards, and existing transpiling languages like Elm, TypeScript, PureScript will continue to improve.
اشتراکها
کتابخانه sir-trevor-js
اشتراکها
فریم ورک bootmetro
گاهی از اوقات نیاز است به همراه مسیریابی، اطلاعاتی را نیز به آنها ارسال کنیم. برای مثال در حین نمایش لیست محصولات، برای هدایت به صفحهی نمایش جزئیات هر محصول، نیاز است Id هر محصول نیز به همراه مسیریابی، به کامپوننت مقصد ارسال شود. اینکار توسط route parameters قابل مدیریت است.
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
این دستورات سبب به روز رسانی خودکار قسمت declarations فایل src\app\product\product.module.ts نیز خواهند شد.
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
اولین شیء مسیریابی تعریف شده را در قسمتهای پیشین بررسی کردیم که امکان نمایش کامپوننت لیست محصولات را توسط یک routerLink در منوی سایت میسر میکند.
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
و آنرا به نحو ذیل تکمیل خواهیم کرد:
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
ذکر پارامتر save در اینجا، سبب به روز رسانی فایل package.json نیز خواهد شد:
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
این کلاس را در ادامه به صورت ذیل تکمیل خواهیم کرد:
همانطور که ملاحظه میکنید، کلاسی که قرار است به عنوان منبع دادهی بستهی angular-in-memory-web-api بکار رود باید InMemoryDbService, InMemoryBackendConfig را نیز پیاده سازی کند که نمونهای از آنرا در اینجا برای بازگشت یک لیست درون حافظهای محصولات، مشاهده میکنید.
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
- ابتدا مکان یافت شدن ماژولهای مورد نیاز ProductData و InMemoryWebApiModule تعریف شدهاند و سپس InMemoryWebApiModule.forRoot را جهت تشکیل یک Web API آزمایشی، برای ارائهی اطلاعات کلاس ProductData، به لیست imports اضافه کردهایم. باید دقت داشت که نیاز است تعریف InMemoryWebApiModule پس از تعریف HttpModule باشد تا بتواند تعدادی از پیش فرضهای آن را بازنویسی کند.
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
که سبب افزوده شدن سرویس product.service.ts و همچنین به روز رسانی خودکار قسمت providers ماژول product.module.ts نیز میشود:
اگر نام ماژول را ذکر نکنیم، سرویس مدنظر تولید خواهد شد، اما قسمت providers هیچ ماژولی به صورت خودکار تکمیل نمیشود.
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
این سرویس HTTP، به سرویس Web API آزمایشی واقع در آدرس baseUrl، متصل خواهد شد:
از آن برای دریافت لیست محصولات (getProducts)، دریافت جزئیات یک محصول (getProduct)، حذف یک محصول (deleteProduct)، ثبت و یا به روز رسانی یک محصول (saveProduct) استفاده خواهیم کرد.
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
در اینجا با استفاده از سرویس محصولاتی که پیشتر ایجاد کردیم، کار دریافت لیست محصولات انجام شده و سپس به خاصیت عمومی products نسبت داده میشود. این خاصیت را در قالب این کامپوننت نمایش خواهیم داد. به همین جهت فایل src\app\product\product-list\product-list.component.html را گشوده و آنرا به نحو ذیل تکمیل کنید:
اکنون اگر برنامه را توسط دستور ng serve -o ساخته و اجرا کنید، چنین صفحهای قابل مشاهده خواهد بود:
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
برای مشاهدهی جزئیات هر محصول نیاز است Id آن محصول را به عنوان پارامتر مسیریابی ارسال کنیم. به همین جهت این Id را به عنوان پارامتری جدید، به routerLink انتساب دادهایم.
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
در اینجا ابتدا root URL segment ذکر میشود. سپس پارامترهای متغیر مسیریابی و همچنین ثوابت آن مسیر خاص نیز باید ذکر شوند. اگر URL segment ثابت edit، در انتها ذکر نشود، این مسیر با تنظیم 'path: 'products/:id تطابق داده خواهد شد و نه با حالت 'path: 'products/:id/edit.
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
در اینجا عدد صفر را به عنوان پارامتر یا Id محصول جدید، به همان صفحهی ویرایش اطلاعات یک محصول، ارسال کردهایم. اگر به سرویس محصولات دقت کنید،
اگر id مساوی صفر باشد، یک محصول جدید ایجاد خواهد شد و اگر غیر صفر باشد، این محصول از پیش موجود، به روز رسانی میگردد.
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
سپس این مسیریابی توسط لینک ذیل فعال میشود:
اکنون برای دریافت مقدار این پارامتر از URL جاری، میتوان از this.route.snapshot.params['id'] استفاده کرد. این id دقیقا نام همان متغیری است که در تعریف مسیریابی ذکر شدهاست و حساس به کوچکی و بزرگی حروف میباشد.
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
هر زمان که پارامترهای مسیریابی تغییر کنند، این Observable به آنها گوش فرا داده و برنامه را از این تغییرات مطلع میسازد.
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
در این حالت اگر آدرس http://localhost:4200/products/1 توسط کاربر درخواست شود، نیاز است بتوان id=1 آنرا از مسیرجاری استخراج کرد. به همین جهت سرویس ActivatedRoute به سازندهی کلاس کامپوننت جزئیات محصول تزریق شدهاست. هرچند میتوان از این سرویس در همان سازندهی کلاس نیز استفاده کرد، اما انجام اعمال async آغازین یک کامپوننت بهتر است به ngOnInit منتقل شوند تا سبب تاخیری در آغاز و نمایش کامپوننت نگردند. این life cycle hook، پس از آغاز کامپوننت فراخوانی میگردد. به همین جهت ذکر implements OnInit را در قسمت تعریف کلاس مشاهده میکنید.
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
در اینجا علاوه بر استفاده از شیء product در جهت نمایش جزئیات محصول انتخابی، دو دکمهی back و edit نیز اضافه شدهاند که اولی صفحهی لیست محصولات را مجددا نمایش میدهد و دومی کار هدایت به صفحهی ویرایش جزئیات این محصول را میسر میکند.
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
کد کامل کامپوننت ویرایش و افزودن جزئیات یک محصول به شرح ذیل است (فایل src\app\product\product-edit\product-edit.component.ts):
به همراه کد کامل قالب آن (فایل src\app\product\product-edit\product-edit.component.html):
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
برنامه را یکبار توسط دستور ng server -o ساخته و اجرا کنید.
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
در اینجا چون کامپوننت به علت نحوهی تعریف مسیریابی آن مجددا آغاز نمیشود، شیء route.snapshot برای دسترسی به پارامترهای تغییر کردهی مسیریابی، کارآیی لازم را نداشته و باید از روش دوم دسترسی به آن مقادیر که یک Observable است و به تغییرات پارامترها گوش فرا میدهد، استفاده کرد.
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
در اینجا دو روش دسترسی به پارامتر id را مشاهده میکنید. در حالت کار با snapshot متد paramMap.get اینبار یک رشته را قبول میکند و یا بجای params میتوان از paramMap استفاده کرد.
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
و سپس میتوان یک چنین لینکی را جهت فعالسازی آن اضافه کرد:
این روش به همراه URLهایی ناخوانا خواهد بود که قسمتهای مختلف آن مشخص نیستند و هر بار که قرار باشد گزینهی دیگری را به جستجو اضافه کرد، نیاز است این پارامترها را نیز تغییر داد. همچنین در حین جستجو ممکن است تعدادی از فیلدها اختیاری باشند و نه اجباری. برای حل این مشکل امکان تعریف پارامترهای اختیاری مسیریابی نیز پیش بینی شدهاست. دراین حالت تعریف مسیریابی صفحهی نمایش لیست محصولات به صورت ذیل خواهد بود (بدون ذکر هیچ پارامتری):
و سپس لینکی که به آن تعریف میشود، نحوهی تعریف خاصی را خواهد داشت:
در اینجا پارامترهای اختیاری به صورت یک سری key/value در آرایهی پارامترهای مسیریابی مشخص میشوند و هربار که نیاز به تغییر آنها بود، نیازی نیست تا تعریف مسیریابی اصلی مرتبط را تغییر داد. باید دقت داشت که پارامترهای اختیاری باید همواره پس از پارامترهای اجباری در این آرایه، ذکر شوند.
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
نحوهی خواندن این پارامترها، دقیقا همانند نحوهی خواندن پارامترهای اجباری هستند و در اینجا از نام keyها برای اشارهی به آنها استفاده میشود:
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
در این لینک جزئیات محصول 5 نمایش داده خواهد شد. پس از عدد 5، پارامترهای کوئری ذکر شدهاند و برخلاف پارامترهای اختیاری مسیریابی، در بین مسیریابی و هدایت کاربران به صفحات مختلف، حفظ خواهند شد.
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
اعمال پارامترهای کوئری مختلف، به لینکهای تعریف شده، توسط دایرکتیو queryParams صورت میگیرد و در اینجا یک مجموعهی از key/valueها ذکر خواهند شد:
در این مثال یک ثابت دلخواه er مشخص شدهاست. بدیهی است میتوان متغیری را نیز بجای این ثابت تعریف کرد (یک خاصیت عمومی تعریف شدهی در سطح کامپوننت که به تکستباکس جستجو متصل است).
و یا با کدنویسی به صورت ذیل است:
باید دقت داشت که چون این پارامترهای کوئری در بین مسیریابی به صفحات مختلف حفظ میشوند، نباید کلیدهای انتخاب شدهی در اینجا با سایر کلیدهای موجود در صفحات دیگر تداخل پیدا کنند.
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
یعنی دکمهی back به این شکل تغییر میکند:
و یا استفاده از { preserveQueryParams: true} در حین کدنویسی.
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
علت تعریف '' || این است که ممکن است filterBy تعریف نشده باشد (برای حالتی که صفحه برای بار اول نمایش داده میشود) و دلیل تعریف 'true' === این است که مقادیر دریافتی در اینجا رشتهای هستند و نه boolean. به همین جهت باید با رشتهی true مقایسه شوند.
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
فایل src\app\product\product-filter.pipe.ts با این محتوا:
و سپس تعریف تکست باکس فیلتر کردن در ابتدای قالب src\app\product\product-list\product-list.component.html :
و اعمال این فیلتر به حلقهی نمایش ردیفهای جدول؛ به همراه تعریف پارامتر کوئری:
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
تنظیم مسیریابیها جهت درج پارامترها
پیش از ارسال اطلاعات مورد نیاز، به مسیری خاص، نیاز است محل قرارگیری این اطلاعات را در تعاریف مسیریابیها مشخص کرد.
در ادامهی مثال این سری، دو کامپوننت جدید جزئیات و ویرایش محصولات را به ماژول محصولات اضافه میکنیم:
>ng g c product/ProductDetail >ng g c product/ProductEdit
در ادامه با مراجعه به فایل src\app\product\product-routing.module.ts، تنظیمات مسیریابی آنرا به شکل ذیل تکمیل خواهیم کرد:
import { ProductEditComponent } from './product-edit/product-edit.component'; import { ProductDetailComponent } from './product-detail/product-detail.component'; import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent }, { path: 'products/:id', component: ProductDetailComponent }, { path: 'products/:id/edit', component: ProductEditComponent } ];
دومین شیء مسیریابی، از مسیر ریشهی یکسانی استفاده میکند (products) که علت آنرا در قسمت قبل با «انتخاب استراتژی مناسب نامگذاری مسیرها» بررسی کردیم. در اینجا id: مکانی را مشخص میکند که قرار است اطلاعاتی را به آن مسیر خاص ارسال کند. در اینجا : به معنای تعریف مکان یک پارامتر اجباری مسیریابی است. به علاوه id یک نام دلخواه است و چون در مثال جاری میخواهیم id محصولات را انتقال دهیم، Id نامگرفتهاست؛ وگرنه هر نام دیگری نیز میتواند باشد.
سومین شیء مسیریابی نیز از مسیر ریشهی یکسانی استفاده میکند و تفاوت آنرا با حالت نمایش جزئیات، با افزودن یک edit/ مشخص کردهایم.
در اینجا هر تعداد متغیر مورد نیاز، قابل تعریف است. برای مثال مسیری مانند orders/:id/items/:itemId با دو متغیر و یا بیشتر نیز قابل تعریف است. فقط باید دقت داشت که نامهای پس از : در یک مسیریابی، باید منحصربفرد باشند.
تکمیل کامپوننت نمایش لیست محصولات
پیش از ادامهی بحث نیاز است کامپوننت خالی لیست محصولات را که در قسمتهای قبل ایجاد کردیم، تکمیل کنیم تا پس از آن بتوانیم لینکهایی را به صفحات نمایش جزئیات محصولات و همچنین ویرایش و افزودن محصولات نیز اضافه کنیم. به همین جهت ابتدا اینترفیس محصول را اضافه میکنیم:
> ng g i product/IProduct
export interface IProduct { id: number; productName: string; productCode: string; }
تشکیل یک منبع اطلاعات درون حافظهای
یکی از بستههای Angular به نام angular-in-memory-web-api، قابلیت تشکیل یک REST Web API ساده را دارد که از آن جهت دریافت، ثبت و به روز رسانی لیست محصولات استفاده خواهیم کرد (بدون نیاز به نوشتن کد سمت سرور خاصی؛ صرفا در جهت ساده سازی ارائهی مطلب).
به همین جهت ابتدا بستهی مرتبط با آنرا نصب کنید:
>npm install angular-in-memory-web-api --save
"dependencies": { "angular-in-memory-web-api": "^0.3.1" },
سپس کلاس ProductData را به ماژول محصولات اضافه میکنیم:
> ng g cl product/ProductData
import { IProduct } from './iproduct'; import { InMemoryDbService, InMemoryBackendConfig } from 'angular-in-memory-web-api'; export class ProductData implements InMemoryDbService, InMemoryBackendConfig { createDb() { let products: IProduct[] = [ { 'id': 1, 'productName': 'Product 1', 'productCode': '0011' }, { 'id': 2, 'productName': 'Product 2', 'productCode': '0023' }, { 'id': 5, 'productName': 'Product 5', 'productCode': '0048' }, { 'id': 8, 'productName': 'Product 8', 'productCode': '0022' }, { 'id': 10, 'productName': 'Product 10', 'productCode': '0042' } ]; return { products }; } }
در آخر برای فعالسازی این REST Web API ساده، تنها کافی است به فایل src\app\app.module.ts مراجعه کرده و کلاس ProductData فوق را معرفی کنیم:
import { ProductData } from './product/product-data'; import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(ProductData, { delay: 1000 }), ProductModule, UserModule, AppRoutingModule ],
- در اینجا یک delay را هم در تنظیمات آن مشاهده میکنید. هدف از تعریف آن، شبیه سازی کند بودن دریافت اطلاعات از یک وب سرور واقعی است.
- این وب سرویس در آدرس api/products قابل دسترسی است.
تعریف سرویس HTTP کار با منبع اطلاعات درون حافظهای
پس از تعریف REST Web API درون حافظهای، یک سرویس HTTP را نیز جهت کار با آن، به برنامه اضافه خواهیم کرد:
>ng g s product/product -m product/product.module
installing service create src\app\product\product.service.spec.ts create src\app\product\product.service.ts update src\app\product\product.module.ts
پس از ایجاد قالب ابتدایی فایل product.service.ts، آنرا به نحو ذیل تکمیل کنید:
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/do'; import 'rxjs/add/operator/catch'; import 'rxjs/add/observable/throw'; import 'rxjs/add/operator/map'; import 'rxjs/add/observable/of'; import { IProduct } from './iproduct'; @Injectable() export class ProductService { private baseUrl = 'api/products'; constructor(private http: Http) { } getProducts(): Observable<IProduct[]> { return this.http.get(this.baseUrl) .map(this.extractData) .do(data => console.log('getProducts: ' + JSON.stringify(data))) .catch(this.handleError); } getProduct(id: number): Observable<IProduct> { if (id === 0) { return Observable.of(this.initializeProduct()); }; const url = `${this.baseUrl}/${id}`; return this.http.get(url) .map(this.extractData) .do(data => console.log('getProduct: ' + JSON.stringify(data))) .catch(this.handleError); } deleteProduct(id: number): Observable<Response> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); const url = `${this.baseUrl}/${id}`; return this.http.delete(url, options) .do(data => console.log('deleteProduct: ' + JSON.stringify(data))) .catch(this.handleError); } saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); } private createProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { product.id = undefined; return this.http.post(this.baseUrl, product, options) .map(this.extractData) .do(data => console.log('createProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private updateProduct(product: IProduct, options: RequestOptions): Observable<IProduct> { const url = `${this.baseUrl}/${product.id}`; return this.http.put(url, product, options) .map(() => product) .do(data => console.log('updateProduct: ' + JSON.stringify(data))) .catch(this.handleError); } private extractData(response: Response) { let body = response.json(); return body.data || {}; } private handleError(error: Response): Observable<any> { console.error(error); return Observable.throw(error.json().error || 'Server error'); } initializeProduct(): IProduct { // Return an initialized object return { id: 0, productName: null, productCode: null }; } }
private baseUrl = 'api/products';
نمایش لیست محصولات
اکنون پس از این مقدمات میتوانیم کامپوننت لیست محصولات را تکمیل کنیم. به همین جهت به قالب ابتدایی src\app\product\product-list\product-list.component.ts مراجعه کرده و آنرا به نحو ذیل تکمیل کنید:
import { ActivatedRoute } from '@angular/router'; import { Component, OnInit } from '@angular/core'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { pageTitle = 'Product List'; errorMessage: string; products: IProduct[]; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { this.productService.getProducts() .subscribe(products => this.products = products, error => this.errorMessage = <any>error); } }
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> <div class="table-responsive"> <table class="table" *ngIf="products && products.length"> <thead> <tr> <th>Product</th> <th>Code</th> </tr> </thead> <tbody> <tr *ngFor="let product of products"> <td><a [routerLink]="['/products', product.id]"> {{product.productName}} </a> </td> <td>{{ product.productCode}}</td> <td> <a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </td> </tr> </tbody> </table> </div> </div> </div>
توضیحات:
پس از تعریف مسیریابیهای صفحات نمایش لیست محصولات و ویرایش محصولات، اکنون نوبت به اتصال آنها به لینکهایی است تا توسط کاربران برنامه مورد استفاده قرار گیرند.
در اینجا با تعریف لینکی، هر محصول را به صفحهی مشاهدهی جزئیات آن متصل کردهایم:
<a [routerLink]="['/products', product.id]"> {{product.productName}} </a>
و یا برای حالت edit نیز به همین صورت 'path: 'products/:id/edit را مقدار دهی کردهایم.
<a class="btn btn-primary" [routerLink]="['/products', product.id, 'edit']"> Edit </a>
به علاوه به فایل src\app\app.component.html نیز مراجعه کرده و لینکی را ذیل لینک لیست محصولات در منوی سایت، جهت افزودن یک محصول جدید اضافه میکنیم:
<li> <a [routerLink]="['/products', 0, 'edit']">Add Product</a> </li>
saveProduct(product: IProduct): Observable<IProduct> { let headers = new Headers({ 'Content-Type': 'application/json' }); let options = new RequestOptions({ headers: headers }); if (product.id === 0) { return this.createProduct(product, options); } return this.updateProduct(product, options); }
همچنین باید دقت داشت که تمام پارامترهای routerLink را با کدنویسی و در متد navigate نیز میتوان بکار برد. برای مثال:
this.router.navigate(['products', this.product.id]);
خواندن و پردازش پارامترهای مسیریابی
تا اینجا لیست محصولات را نمایش دادیم و همچنین لینکهایی را به صفحات نمایش جزئیات، ویرایش و افزودن این محصولات، تعریف کردیم. مرحلهی بعد، پیاده سازی این کامپوننتها است.
مسیریاب Angular، پارامترهای هر مسیر را توسط سرویس ActivatedRoute استخراج کرده و در اختیار کامپوننتها قرار میدهد. بنابراین برای دسترسی به آنها تنها کافی است این سرویس را به سازندهی کلاس کامپوننت خود تزریق کنیم. پس از آن دو روش برای خواندن اطلاعات مسیرجاری توسط این سرویس فراهم میشود:
الف) استفاده از شیء this.route.snapshot که وضعیت آغازین مسیریابی را به همراه دارد. برای مثال جهت دسترسی به مقدار پارامتر id میتوان به صورت ذیل عمل کرد:
let id = +this.route.snapshot.params['id'];
بنابراین ابتدا یک مسیریابی به همراه پارامتر یا پارامترهایی متغیر تعریف میشود:
{ path: 'products/:id', component: ProductDetailComponent }
<a [routerLink]="['/products', product.id]">{{product.productName}}</a>
ب) این سرویس ویژه به همراه خاصیت this.route.params که یک Observable است نیز میباشد:
this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } );
یک نکته: ذکر علامت + در اینجا (params['id']+) سبب تبدیل رشتهی دریافتی، به عدد میگردد.
تکمیل کامپوننت نمایش جزئیات یک محصول
در ادامه قالب ابتدایی مشاهدهی جزئیات یک محصول را که در فایل src\app\product\product-detail\product-detail.component.ts قرار دارد، گشوده و به نحو ذیل تکمیل کنید:
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-detail', templateUrl: './product-detail.component.html', styleUrls: ['./product-detail.component.css'] }) export class ProductDetailComponent implements OnInit { pageTitle = 'Product Detail'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number) { this.productService.getProduct(id).subscribe( product => this.product = product, error => this.errorMessage = <any>error); } }
در متد OnInit، مقدار id، از snapshot دریافت میگردد. سپس این id به متد getProduct ارسال میشود تا از RES Web API سرویس برنامه، جزئیات این محصول را واکشی کند و به خاصیت product نسبت دهد.
برای تکمیل قالب این کامپوننت نیز فایل src\app\product\product-detail\product-detail.component.html را گشوده و به نحو ذیل تغییر دهید تا در آن بتوان از خاصیت عمومی product استفاده کرد:
<div class="panel panel-primary" *ngIf="product"> <div class="panel-heading" style="font-size:large"> {{pageTitle + ": " + product.productName}} </div> <div class="panel-body"> <div> Name: {{product.productName}} </div> <div> Code: {{product.productCode}} </div> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> <div class="panel-footer"> <a class="btn btn-default" [routerLink]="['/products']"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a> <a class="btn btn-primary" style="width:80px;margin-left:10px" [routerLink]="['/products', product.id, 'edit']"> Edit </a> </div> </div>
تکمیل کامپوننت ویرایش و افزودن جزئیات یک محصول
از آنجائیکه قصد داریم به ماژول محصولات فرم جدیدی را اضافه کنیم، نیاز است به فایل src\app\product\product.module.ts مراجعه کرده و FormsModule را به قسمت imports آن اضافه کنیم:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, ProductRoutingModule ]
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ProductService } from './../product.service'; import { IProduct } from './../iproduct'; @Component({ selector: 'app-product-edit', templateUrl: './product-edit.component.html', styleUrls: ['./product-edit.component.css'] }) export class ProductEditComponent implements OnInit { pageTitle = 'Product Edit'; product: IProduct; errorMessage: string; constructor(private productService: ProductService, private route: ActivatedRoute, private router: Router) { } ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); } getProduct(id: number): void { this.productService.getProduct(id) .subscribe( (product: IProduct) => this.onProductRetrieved(product), (error: any) => this.errorMessage = <any>error ); } onProductRetrieved(product: IProduct): void { this.product = product; if (this.product.id === 0) { this.pageTitle = 'Add Product'; } else { this.pageTitle = `Edit Product: ${this.product.productName}`; } } deleteProduct(): void { if (this.product.id === 0) { // Don't delete, it was never saved. this.onSaveComplete(); } else { if (confirm(`Really delete the product: ${this.product.productName}?`)) { this.productService.deleteProduct(this.product.id) .subscribe( () => this.onSaveComplete(`${this.product.productName} was deleted`), (error: any) => this.errorMessage = <any>error ); } } } saveProduct(): void { if (true === true) { this.productService.saveProduct(this.product) .subscribe( () => this.onSaveComplete(`${this.product.productName} was saved`), (error: any) => this.errorMessage = <any>error ); } else { this.errorMessage = 'Please correct the validation errors.'; } } onSaveComplete(message?: string): void { if (message) { // TODO: show msg } // Navigate back to the product list this.router.navigate(['/products']); } }
<div class="panel panel-primary"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body" *ngIf="product"> <form class="form-horizontal" novalidate (ngSubmit)="saveProduct()" #productForm="ngForm"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (productNameVar.touched || productNameVar.dirty) && !productNameVar.valid }"> <label class="col-md-2 control-label" for="productNameId">Product Name</label> <div class="col-md-8"> <input class="form-control" id="productNameId" type="text" placeholder="Name (required)" required minlength="3" [(ngModel)]=product.productName name="productName" #productNameVar="ngModel" /> <span class="help-block" *ngIf="(productNameVar.touched || productNameVar.dirty) && productNameVar.errors"> <span *ngIf="productNameVar.errors.required"> Product name is required. </span> <span *ngIf="productNameVar.errors.minlength"> Product name must be at least three characters. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (productCodeVar.touched || productCodeVar.dirty) && !productCodeVar.valid }"> <label class="col-md-2 control-label" for="productCodeId">Product Code</label> <div class="col-md-8"> <input class="form-control" id="productCodeId" type="text" placeholder="Code (required)" required [(ngModel)]=product.productCode name="productCode" #productCodeVar="ngModel" /> <span class="help-block" *ngIf="(productCodeVar.touched || productCodeVar.dirty) && productCodeVar.errors"> <span *ngIf="productCodeVar.errors.required"> Product code is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!productForm.valid" (click)="saveProduct()"> Save </button> </span> <span> <a class="btn btn-default" [routerLink]="['/products']"> Cancel </a> </span> <span> <a class="btn btn-default" (click)="deleteProduct()"> Delete </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
توضیحات:
نکتهی مهمی را که در این کدها میخواهیم بررسی کنیم، متد ngOnInit آناست:
ngOnInit(): void { let id = +this.route.snapshot.params['id']; this.getProduct(id); }
- ابتدا لیست محصولات را مشاهده کنید.
- سپس بر روی دکمهی edit محصول شماره یک کلیک نمائید:
تصویر فوق حاصل خواهد شد که صحیح است. اطلاعات مربوط به محصول یک از وب سرور آزمایشی برنامه واکشی شده و به شیء product نسبت داده شدهاست. همین انتساب سبب مقدار دهی فیلدهای فرم ویرایش اطلاعات گردیدهاست.
- در ادامه بر روی لینک Add product در منوی بالای صفحه کلیک کنید:
همانطور که مشاهده میکنید، هرچند URL صفحه تغییر کردهاست، اما هنوز فرم ویرایش اطلاعات، به روز نشده و فیلدهای آن جهت درج یک محصول جدید خالی نشدهاند. علت اینجا است که در متد ngOnInit، مقدار پارامتر id را از طریق شیء route.snapshot دریافت کردهایم. اگر تنها پارامترهای URL تغییر کنند، کامپوننت مجددا آغاز نشده و متد ngOnInit فراخوانی نمیشود. در اینجا تغییر آدرس http://localhost:4200/products/1/edit به http://localhost:4200/products/0/edit به علت عدم تغییر root URL segment آن یا همان products، سبب فراخوانی مجدد ngOnInit نمیشود. به همین جهت است که فیلدهای این فرم تغییر نکردهاند.
برای حل این مشکل بجای دریافت پارامترهای مسیریابی از طریق شیء route.snapshot بهتر است به تغییرات آنها گوش فرا دهیم. اینکار را از طریق متد route.params.subscribe میتوان انجام داد:
ngOnInit(): void { this.route.params.subscribe( params => { let id = +params['id']; this.getProduct(id); } ); }
یک نکته: هر زمانیکه از Observableها استفاده میشود، نیاز است در پایان کار کامپوننت، unsubscribe آن نیز فراخوانی شود تا نشتی حافظه رخ ندهد. در اینجا یک سری استثناء هم وجود دارند، مانند this.route.params که به صورت خودکار توسط طول عمر سرویس ActivatedRoute مدیریت میشود.
روش خواندن پارامترهای مسیریابی در +Angular 4
روشی که تا به اینجا در مورد خواندن پارامترهای مسیریابی ذکر شد، با Angular 4 هم سازگار است. در Angular 4 روش دیگری را نیز اضافه کردهاند که توسط شیء paramMap مدیریت میشود:
// For Angular 4+ let id = +this.route.snapshot.paramMap.get('id'); this.getProduct(id); // OR this.route.paramMap.subscribe(params => { let id = +params['id']; this.getProduct(id); });
تعریف پارامترهای اختیاری مسیریابی
فرض کنید یک صفحهی جستجو را طراحی کردهاید که در آن میتوان نام و کد محصول را جستجو کرد. سپس میخواهیم این پارامترها را به صفحهی نمایش لیست محصولات هدایت کنیم. برای طراحی این نوع مسیریابی میتوان از مطالبی که تاکنون گفته شد استفاده کرد و پارامترهایی اجباری را جهت مسیریابی تعریف نمود:
{ path: 'products/:name/:code', component: ProductListComponent }
[routerLink]="['/products', productName, productCode]"
{ path: 'products', component: ProductListComponent },
[routerLink]="['/products', {name: productName, code: productCode}]"
در این حالت لینک تولید شده چنین شکلی را خواهد داشت:
http://localhost:4200/products;name=Controller;code=gmg
let name = this.route.snapshot.params['name']; let code = this.route.snapshot.params['code'];
این پارامترها پس از هدایت کاربر به مسیری دیگر، حفظ نشده و پاک خواهند شد. به همین جهت کلیدهای تعریف شدهی در اینجا ضرورتی ندارد که یکتا بوده و با سایر قسمتهای برنامه تداخلی نداشته باشند.
تعریف پارامترهای کوئری در مسیریابی
فرض کنید لیست محصولات را بر اساس پارامتر یا پارامترهایی فیلتر کردهاید. اکنون اگر بر روی لینک مشاهدهی جزئیات یک محصول یافت شده کلیک کنید و سپس مجددا به لیست فیلتر شده بازگردید، تمام پارامترهای انتخابی پاک شده و لیست از ابتدا نمایش داده میشود. در یک چنین حالتی برای بهبود تجربهی کاربری، بهتر است پارامترهای جستجو شده را در طی هدایت کاربر به قسمتهای مختلف حفظ کرد:
http://localhost:42000/products/5?filterBy=product1
در اینجا نیز برای تعریف یک چنین قابلیتی، مسیریابی ابتدایی تعریف شده، همانند قبل خواهد بود و در آن خبری از پارامترهای کوئری نیست:
{ path: 'products', component: ProductListComponent}
<a [routerLink] = "['/products', product.id]" [queryParams] = "{ filterBy: 'er', showImage: true }"> {{product.productName}} </a>
و یا با کدنویسی به صورت ذیل است:
this.router.navigate(['/products'], { queryParams: { filterBy: 'er', showImage: true} } );
یک مشکل: اگر در صفحهی نمایش جزئیات یک محصول، دکمهی Back وجود داشته باشد، با کلیک بر روی آن پارامترهای کوئری پاک خواهند شد و دیگر حفظ نمیشوند. مرحلهی آخر حفظ پارامترهای کوئری در بین صفحات مختلف تنظیم ذیل است:
[preserveQueryParams] = "true"
<a class="btn btn-default" [routerLink]="['/products']" [preserveQueryParams]="true"> <i class="glyphicon glyphicon-chevron-left"></i> Back </a>
که البته در Angular 4 مورد اول به "queryParamsHandling= "preserve و مورد دوم به { 'queryParamsHandling: 'preserve} تغییر یافتهاست و حالت قبلی منسوخ شده اعلام گردیدهاست.
this.router.navigate(['/products'], { queryParamsHandling: 'preserve'} );
پس از بازگشت به صفحهی اصلی لیست محصولات، هرچند این پارامترهای کوئری حفظ شدهاند، اما مجددا به صورت خودکار پردازش نخواهند شد. برای خواندن آنها در متد ngOnInit باید به صورت ذیل عمل کرد:
var filter = this.route.snapshot.queryParams['filterBy'] || ''; var showImage = this.route.snapshot.queryParams['showImage'] === 'true';
در مثال تکمیل شدهی جاری، امکان فیلتر کردن جدول با اضافه کردن یک pipe جدید به نام ProductFilter میسر شدهاست:
>ng g p product/ProductFilter
import { PipeTransform, Pipe } from '@angular/core'; import { IProduct } from './iproduct'; @Pipe({ name: 'productFilter' }) export class ProductFilterPipe implements PipeTransform { transform(value: IProduct[], filterBy: string): IProduct[] { filterBy = filterBy ? filterBy.toLocaleLowerCase() : null; return filterBy ? value.filter((product: IProduct) => product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1) : value; } }
<div class="panel-body"> <div class="row"> <div class="col-md-2">Filter by:</div> <div class="col-md-4"> <input type="text" [(ngModel)]="listFilter" /> </div> </div> <div class="row" *ngIf="listFilter"> <div class="col-md-6"> <h3>Filtered by: {{listFilter}} </h3> </div> </div>
<tr *ngFor="let product of products | productFilter:listFilter"> <td><a [routerLink]="['/products', product.id]" [queryParams]="{filterBy: listFilter}"> {{product.productName}} </a> </td>
در اینجا اگر به صفحهی جزئیات محصول فیلتر شده مراجعه کنیم، این فیلتر حفظ خواهد شد:
و در این حالت اگر بر روی دکمهی back کلیک کنیم، مجددا فیلتر وارد شده بازیابی شده و نمایش داده میشود:
که برای میسر شدن آن ابتدا خاصیت عمومی listFilter به کامپوننت لیست محصولات اضافه گردیده و سپس در ngOnInit، این پارامتر در صورت وجود، از سیستم مسیریابی دریافت میشود:
@Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { listFilter: string; ngOnInit(): void { this.listFilter = this.route.snapshot.queryParams['filterBy'] || '';
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-02.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
چند سال قبل یک گوشی Samsung galaxy J7 را که به صورت پیشفرض به همراه اندروید 6 است، تهیه کردم. الان حدود دو هفتهای است که اندروید 10 را از طریق LineageOS بر روی این گوشی نصب کردهام که در ادامه روش نصب آنرا برای علاقمندان توضیح خواهم داد.
LineageOS چیست؟
یکی از مهمترین مزایای استفاده از گوشیهای اندرویدی نسبت به iOS ای، آزادی نصب نرم افزار و خصوصا سیستم عاملهای مختلف بر روی آنها است. در حال حاضر، محبوبترین و پر استفادهترین نگارش آزاد اندروید که مستقل از گوگل عمل میکند، LineageOS نامدارد (لینیایج OS) که پیشتر با نامهای CyanogenMod و قبل از آن، Cyanogen (سیانوژن) ارائه میشد. یکی از مهمترین مزایای آن، امکان نصب آخرین نگارش اندروید بر روی گوشیهایی است که دیگر پشتیبانی رسمی نمیشوند و خط تولید آنها خاتمه یافتهاست.
به LineageOS یک Custom ROM هم گفته میشود. ROM مخفف read-only memory است و دقیقا جائیاست که هستهی Android در آن مشغول به کار است. بنابراین منظور از Custom ROM، همان نگارش سفارشی از Android است. به عملیات نصب LineageOS در اصطلاح Flashing هم گفته میشود که به معنای بازنویسی قسمتی از یک نرمافزار، با نرمافزار دیگری است.
پیشنیازهای ضروری نصب LineageOS
- داشتن یک گوشی یا تبلت سازگار با آن (متاسفانه سایت lineageos.org با IP ایرانی باز نمیشود)
- دسترسی به یک کابل USB مخصوص گوشی
- داشتن یک کامپیوتر دسکتاپ و یا لپتاپ
- دسترسی به اینترنت
- زمان! ... (انجام این عملیات برای من در بار اول، حدودا یک روز طول کشید! (صرف نظر از تحقیقات یک هفتهای روش انجام آن) البته نه به علت طولانی بودن زمان نصب آن، بلکه به علت وجود نکات ریزی که در هیچ مستنداتی، به صورت مدون پیدا نخواهید کرد و عدم آشنایی با آنها ممکن است سبب بروز حملهی قلبی، به علت در دست داشتن سخت افزاری شود که هم اکنون کل آنرا فرمت کردهاید و ... راهنماهای ارائه شدهی در اینترنت هم بر روی آن کار نمیکنند! به یک چنین سخت افزاری، brick یا «پاره آجر» هم گفته میشود!)
دریافت Custom ROM سازگار با گوشی یا تبلت
مرحلهی اول نصب LineageOS، دریافت Custom ROM آن است. برای این منظور به آدرس download.lineageos.org مراجعه کرده و ابتدا از منوی سمت چپ صفحه، گوشی خود را پیدا کنید و سپس با انتخاب آن، امکان دریافت ROM مخصوص آنرا خواهید یافت.
نکتهی مهم! متاسفانه در اولین دریافت من از این سایت، به علت ناقص بودن دانلود، فایل دریافتی به همراه CRC Error بود و در زمان نصب فایل zip آن، خطای کلی e1001 ظاهر شد و نه هیچ چیز دیگری. این لحظه واقعا لحظهای است که ممکن است عرق سرد بر روی پیشانی شما ظاهر شود! به صورت اتفاقی با بررسی فایل zip آن بر روی کامپیوتر متوجه شدم که فایل، ناقص دریافت شده. به همین جهت پیش از شروع به نصب، فایل zip را در یک برنامهی باز کنندهی آنها مانند winrar و یا 7-zip باز کرده و بر روی دکمهی test آنها کلیک کنید. اگر خطایی را گزارش ندادند، شروع به ادامهی مراحل نصب کنید.
دریافت فایل Recovery سفارشی
در اینجا نیاز است با دو واژهی جدید bootloader و recovery آشنا شد. زمانیکه گوشی خودتان را روشن میکنید، اولین نرم افزاری که حتی پیش از سیستم عامل اجرا میشود، bootloader نام دارد که کار آن آغاز سایر پروسهها است. بعد از بارگذاری بوتلودر، برنامهی دیگری به نام recovery، کار بارگذاری سیستم عامل را انجام میدهد. بوتلودر و recovery پیشفرض اندروید، اجازهی نصب یک custom ROM را نمیدهند. به همین جهت نیاز است این برنامهی recovery را با یک نمونهی سفارشی بازنویسی کرد که این نمونهی سفارشی در اینجا TWRP نام دارد و نمونهی مخصوص گوشی خود را میتوانید با جستجوی در لیست https://twrp.me/Devices دریافت کنید. ابتدا نوع گوشی و سپس مدل آنرا یافته و سپس در صفحهای که ظاهر میشود، بر روی download link آن کلیک کنید تا لیست فایلهای موجود ظاهر شوند. در ابتدا آخرین نگارش موجود را دریافت کنید.
یک تجربه! متاسفانه آخرین نگارش TWRP دریافت شده، بر روی گوشی من کار نکرد و پس از نصب آن، مدام وارد همان سیستم عامل قبلی، با ارائهی پیام «Recovery is NOT SEANDROID Enforcing» میشد و هیچ تاثیری را نداشت. در این حالت نصب نگارش قدیمیتر 3.3.1، کار کرد. بنابراین بهتر است چندین نگارش آنرا دریافت کنید؛ تا در صورت لزوم بتوانید یکی یکی، آنها را آزمایش کنید.
دریافت Google Apps
LineageOS به همراه برنامههای گوگل، مانند play store و امثال اینها نیست. به همین جهت نیاز است آنها را از آدرس https://opengapps.org دریافت کنید. در اینجا دقت داشته باشید که چه چیزی را انتخاب میکنید! برای نمونه برای گوشی من گزینههای ARM، نگارش 10 و pico انتخاب شدند و سپس کلیک بر روی دکمهی دانلود. گزینهی pico، یکی از کم حجمترین نگارشها است و همینقدر برای شروع به کار، کافی است. نگارش را 10 انتخاب میکنیم چون میخواهیم اندروید 10 را نصب کنیم و انتخاب معماری CPU گوشی هم مهم است. با استفاده از برنامهای مانند device info، به برگهی CPU آن مراجعه کرده و CPU Type گوشی خود را دقیق بررسی کنید. اگر مانند گوشی من، 32bit بود، باید ARM را انتخاب کنید و اگر 64bit بود، گزینهی ARM64 را انتخاب کنید و اگر یک گوشی قدیمی را مانند ASUS دارید، ممکن است CPU آن از نوع intel و x86 باشد.
دریافت برنامهی فعالسازی دسترسی root
اگر میخواهید دسترسی root هم داشته باشید (این گزینه اختیاری است و من آنرا نصب نکردم)، در نگارشهای قبلی LineageOS از برنامهای به نام SU برای انجام اینکار استفاده میشد. این برنامه دیگر نگهداری نمیشود و نباید آنرا به همراه آخرین نگارش LineageOS نصب کرد (خیلی مهم!)؛ وگرنه گوشی شما را حتما به هم خواهد ریخت. برنامهی جایگزین آن Magisk نام دارد که باز هم من آنرا توصیه نمیکنم! چون اگر به انجمنهای LineageOS مراجعه کنید، مشاهده شدهاست که پس از نصب به روز رسانیهای جدید هفتگی LineageOS، ممکن است به علت عدم سازگاری با Magisk، سیستم عامل گوشی شما بالا نیاید و در یک حلقهی بیپایان قرار بگیرید. به همین جهت بهتر است از این گزینه صرفنظر کنید.
آماده سازی گوشی برای اتصال USB و اجرای فرامین بر روی آن
مرحلهی بعد، نصب برنامهی recovery سفارشی است. برای اینکار نیاز است گوشی خود را توسط سیم USB، به یک کامپیوتر متصل کرده و سپس توسط برنامهای خاص که در ادامه معرفی میشود، برنامهی TWRP را بر روی آن نصب کرد. به همین جهت به قسمت «تنظیمات» گوشی اندرویدی خود رفته و گزینهی «دربارهی دستگاه (About)» را پیدا کنید. سپس بر روی شمارهی build آن «Build Number»، هفت بار ضربه بزنید. اینکار سبب میشود تا یک منوی مخفی به نام «Developer Mode» یا «گزینههای توسعه دهندگان/برنامه نویسان»، به لیست منوهای تنظیمات سیستم عامل فعلی اضافه شود. پس از فعال شدن «Developer Mode»، به این گزینه وارد شده و دو گزینهی زیر را در آن فعال کنید:
- USB debugging
- OEM unlocking
اکنون اگر گوشی خود را از طریق سیم USB به کامپیوتر متصل کنید، یک دیالوگ باکس پرسشی، در اندروید جاری ظاهر میشود که درخواست دسترسی به ADB را از شما سؤال میپرسد. گزینهی «Always Allow From This Computer» را انتخاب کرده و با کلیک بر روی OK، این دسترسی را فعال کنید.
دریافت برنامههای انتقال اطلاعات به گوشی اندرویدی
پس از دریافت فایلهای مورد نیاز (TWRP.img, firmware.zip و gapps.zip)، اکنون نوبت به نصب TWRP.img است تا برنامهی recovery پیشفرض گوشی را با یک نمونهی سفارشی که امکان نصب custom ROM را میسر میکند، بازنویسی کنیم. بر روی گوشیهای سامسونگ، برنامهی ODIN یک چنین قابلیتی را به همراه دارد.
البته اگر کمی جستجو کنید، به دستورات زیر هم خواهید رسید که توسط برنامهی Minimal_ADB_Fastboot قابل اجرا هستند:
من تمام این دستورات را آزمایش کردم و بر روی گوشی سامسونگ کار نکردند! اما ODIN کار کرد.
البته برنامهی Minimal_ADB_Fastboot، برنامهی بسیار مفیدی است و در ادامه کاربردهایی از آنرا مطالعه خواهید کرد.
بررسی امنیتی مهم! آیا فایل ROM دریافت شده، بر روی گوشی من نصب میشود؟!
در ادامه پیش از نصب، یکبار گوشی را فرمت میکنیم. در این حال اگر در حین نصب، پیام سازگار نبودن فایل ROM را دریافت کنیم، بسیار دیر است! به همین جهت پس از نصب برنامهی Minimal_ADB_Fastboot، به پوشهی آن وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید (با این فرض که گوشی شما از طریق سیم USB به کامپیوتر متصل است و همچنین دسترسی دیباگی را هم که در گوشی عنوان شد، دادهاید):
دستور اول، adb server را اجرا کرده و سیستم شما را به گوشی متصل میکند. همچنین یک id را هم نمایش میدهد که نشان از موفقیت آمیز بودن اتصال دارد. دو دستور بعدی، شماره دستگاه و مدل آنرا بازگشت میدهند. این خروجیها را به دقت بخاطر بسپرید.
سپس فایل custom ROM دریافت شده را باز کرده و به پوشهی «META-INF\com\google\android» آن وارد شوید. در اینجا فایل متنی updater-script را باز کنید. برای مثال در مورد گوشی من، چنین سطری در ابتدای آن درج شده:
این سطر، دقیقا بررسی میکند که اگر خاصیتهای ro.build.product یا ro.product.device مساوی j7elte نبودند، کل عملیات نصب، abort شود.
بنابراین حتما پیش از مطالعه و اجرای ادامهی بحث، مقادیر این ویژگیها را با سطر اول فایل updater-script انطباق دهید تا اگر یکی نبودند، به اشتباه گوشی خود را فرمت نکنید!
البته در جائی دیدم که عدهای برای «خوراندن» rom سفارشی دریافت شده، این سطر بررسی را از فایل یاد شده، پاک کرده و سپس فایل zip جدیدی را تولید و نصب کردهاند. بهتر است اینکار را نکنید و با جستجوی دقیق مطمئن شوید که یک چنین تغییری، برای سیستم شما مشکلی را ایجاد نمیکند!
بازنویسی برنامهی recovery گوشی توسط ODIN
پس از دریافت برنامهی odin، نیاز است گوشی خود را خاموش کنید. فرض بر این است که پیشتر حداقل از contacts خود پشتیبان تهیه کردهاید. چون از این قسمت به بعد، به مراحل بدون بازگشتی قدم خواهیم گذاشت و قرار است گوشی را کاملا فرمت کنیم!
پس از خاموش کردن گوشی، اکنون نیاز است گوشی را در حالت download بالا بیاوریم. برای اینکار سه دکمهی Volume Down + Home + Power با هم بفشارید. بنابراین ابتدا دکمهی «کاهش صدا» را نگه دارید و رها نکنید، سپس دکمهی home را نگه دارید و رها نکنید و در آخر دکمهی power را نگه دارید تا گوشی به حالت ویژهی download وارد شود.
البته در ابتدا یک صفحهی اخطار را نمایش میدهد که در آن درج شده برای ادامه نیاز است دکمهی «افزایش صدا» را بفشارید.
پس از ظاهر شدن تصویر فوق، اینبار دکمهی «افزایش صدا» را بفشارید تا وارد حالت دانلود شوید. در اینجا «حالت دانلود» یعنی گوشی قابلیت دریافت فایلی را پیدا کردهاست.
- در اینجا در قسمت AP، فایل tar مربوط به TWRP را انتخاب کنید.
- سپس در برگهی options، تیک گزینهی Auto reboot را بردارید (بسیار مهم!). اگر این تیک را برندارید، پس از کار نوشتن برنامهی recovery سفارشی، گوشی شما reboot شده و ... وارد برنامهی recovery ... نمیشود! چون سیستم امنیتی توکار اندروید، آنرا با نمونهی اصلی جایگزین میکند!
- اکنون بر روی دکمهی start کلیک کنید تا کار بازنویسی شروع شود.
پس از پایان بازنویسی برنامهی recovery، باید وارد این برنامهی جدید بشویم که روش ورود به آن به صورت زیر است:
پس از پایان بازنویسی، در بعضی از گوشیها در همین حالت که گوشی، حالت download را نمایش میدهد، اگر ترکیب کلیدهای «Volume Up + Power + Home» را بفشارید (اینبار دکمهی «افزایش صدا» است و نه کاهش صدا)، وارد این برنامهی recovery جدید میشوید. اما در مورد گوشی من چنین چیزی رخ نداد. در این حالت تنها روشی که پاسخ داد، «خارج کردن باطری گوشی» بود (در همین حالتی که صفحهی آبی رنگ download نمایش داده میشود، باطری را خارج کنید)؛ چون حتی در حالت خاموش کردن معمولی هم برنامهی recovery سفارشی را پاک و نمونهی اصلی را جایگزین میکرد!
سپس سیستم را به صورت معمولی روشن نکنید. اینبار نیاز است وارد منوی recovery شویم. بنابراین مجددا باطری را قرار داده و اینبار با فشردن ترکیب کلیدهای «Volume Up + Power + Home» به منوی جدید recovery وارد خواهیم شد.
مرحلهی آخر! نصب سیستم عامل جدید و برنامههای گوگل
تا اینجا باید وارد منوی recovery جدید شده باشید. روش خارج کردن باطری را هم فراموش نکنید! (چون اگر سیستم به صورت معمولی ریاستارت شود، یا حتی به صورت معمولی خاموش شود، برنامهی recovery سفارشی را بیاثر کرده و پاک میکند)
- پس از بارگذاری برنامه، پیام «Swipe to Allow Modifications» ظاهر میشود. برای این منظور، فلش آبی رنگ ظاهر شده را به سمت راست بکشید تا بتوانید وارد برنامه شوید.
- اکنون این مراحل را طی کنید:
الف) انتخاب Wipe
در اینجا در ابتدا گزینهی Format Data را انتخاب کنید.
سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار فرمت کردن سیستم شروع شود.
در ادامه در همین قسمت گزینهی Advanced Wipe را انتخاب کرده (همیشه با انتخاب دکمهی back میتوان به منوی اصلی و گزینههای آن رسید) و Dalvik / ART Cache,Data, System, Cache, Internal storage را انتخاب کنید. سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار پاک کردن سیستم شروع شود. در اینجا همه چیز را منهای SD Card، پاک خواهیم کرد. بدون انجام اینکار، وارد یک حلقهی بینهایت خواهید شد و سیستم اصلی پس از نصب، راه اندازی نمیشود (آزمایش کردم!).
ب) انتقال فایلهای Custom ROM و GApps به گوشی
اکنون به کامپیوتر خود و پوشهی محل نصب برنامهی Minimal_ADB_Fastboot وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید تا فایلها به گوشی منتقل شوند:
دو نکته:
1- فایلهای custom ROM و GApps دریافت شده را به درون پوشهی Minimal_ADB_Fastboot کپی کنید. در اینجا منظور از LINEAGE.zip نام کاملی مانند lineage-17.1-20210114-nightly-j7elte-signed.zip است که دریافت کردهاید و همچنین منظور از GAPPS.zip، نام کاملی مانند open_gapps-arm-10.0-pico-20210116.zip است.
2- برای اجرای این دستورات، نیازی به داشتن یک sdcard نیست. نامی که در اینجا ذکر شده، فقط یک نام پوشهی جدید، در گوشی شما است که قرار است در ادامه فایلها را از آن انتخاب کنیم.
یک نکتهی تکمیلی: در حالت منوی recovery و بعد از پاک کردن همه چیز، اگر فولدرهای گوشی در windows explorer مشخص نیستند، باید آنها را mount کرد تا بشود فایلها را به آنها کپی کرد (روش دوم کپی کردن فایلها به گوشی). اگر به منوی ابتدایی TWRP دقت کنید، یک گزینهی mount هم دارد که دقیقا برای همین منظور است. پوشهها را که mount کردید، در windows explorer جهت کپی کردن معمولی ظاهر میشوند.
ج) نصب نهایی سیستم عامل و برنامههای گوگل
پیش از هر کاری به گزینهی Settings در برنامهی TWRP مراجعه کرده و در برگهی General آن، تیک زیر را بر دارید: Prompt to install TWRP app if not installed!
اکنون که فایلهای custom ROM و GApps به گوشی کپی شدند، از منوی اصلی TWRP، اینبار گزینهی Install را انتخاب کنید (همانطور که عنوان شد، در اینجا همیشه دکمهی back، برای بازگشت به صفحهی اصلی کار میکند).
اگر از طریق دستورات adb فایلها را به پوشهی sdcard منتقل کرده باشید، به صورت خودکار اولین فایل انتخاب شده همان فایل ROM است. سپس بر روی دکمهی «Add more zips» کلیک کرده و فایل zip مربوط به GApps را انتخاب کنید. در بالای صفحه «two of max 10 File queued» باید ظاهر شده باشد (مهم) که به معنای تعداد فایلهای موجود در صف نصب است. اکنون فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار نصب شروع شود.
پس از پایان نصب این دو برنامه، یکبار بر روی دکمهی Wipe cache/dalvik کلیک کنید (به همراه به سمت راست کشیدن دکمهی فلش آبی پایین صفحه) و سپس بر روی دکمهی Reboot System تا ... وارد اندروید 10 شوید!
یک نکته: در اینجا در حین reboot سؤال میپرسد که آیا نیاز است TWRP را نیز به صورت جداگانهای نصب کند. عنوان کنید، خیر.
چگونه به روز رسانیهای LineageOS را نصب کنیم؟
LineageOS هفتهای یکبار، آخرین به روز رسانیهای اندروید را توزیع میکند. برای نصب آنها پیامی را ظاهر کرده و امکان دانلود را فراهم میکند. پس از دانلود، اگر بر روی دکمهی install کلیک کنید، به صورت خودکار شما را وارد منوی recovery فوق میکند (و نه نصب خودکار). در اینجا تنها کاری را که باید انجام دهید، انتخاب گزینهی install است و سپس انتخاب پوشهی data/lineageos_updates که محل قرار گیری این فایل zip دریافت شدهاست. با انتخاب فایل zip، مراحل نصب آن مانند قبل است. پس از پایان نصب، یکبار بر روی دکمهی پاک کردن کش dalvik کلیک کنید و سپس بر روی reboot. کش dalvik همواره به صورت خودکار توسط اندروید ساخته میشود و پاک کردن آن مشکلی را ایجاد نمیکند.
پس از راه اندازی مجدد سیستم، به منوی Settings>about phone>lineageOS مراجعه کرده و فایل zip قدیمی را حذف کنید (در همان صفحهای که پیام دریافت و نصب را نمایش میداد، اکنون پیام delete ظاهر شدهاست).
LineageOS چیست؟
یکی از مهمترین مزایای استفاده از گوشیهای اندرویدی نسبت به iOS ای، آزادی نصب نرم افزار و خصوصا سیستم عاملهای مختلف بر روی آنها است. در حال حاضر، محبوبترین و پر استفادهترین نگارش آزاد اندروید که مستقل از گوگل عمل میکند، LineageOS نامدارد (لینیایج OS) که پیشتر با نامهای CyanogenMod و قبل از آن، Cyanogen (سیانوژن) ارائه میشد. یکی از مهمترین مزایای آن، امکان نصب آخرین نگارش اندروید بر روی گوشیهایی است که دیگر پشتیبانی رسمی نمیشوند و خط تولید آنها خاتمه یافتهاست.
به LineageOS یک Custom ROM هم گفته میشود. ROM مخفف read-only memory است و دقیقا جائیاست که هستهی Android در آن مشغول به کار است. بنابراین منظور از Custom ROM، همان نگارش سفارشی از Android است. به عملیات نصب LineageOS در اصطلاح Flashing هم گفته میشود که به معنای بازنویسی قسمتی از یک نرمافزار، با نرمافزار دیگری است.
پیشنیازهای ضروری نصب LineageOS
- داشتن یک گوشی یا تبلت سازگار با آن (متاسفانه سایت lineageos.org با IP ایرانی باز نمیشود)
- دسترسی به یک کابل USB مخصوص گوشی
- داشتن یک کامپیوتر دسکتاپ و یا لپتاپ
- دسترسی به اینترنت
- زمان! ... (انجام این عملیات برای من در بار اول، حدودا یک روز طول کشید! (صرف نظر از تحقیقات یک هفتهای روش انجام آن) البته نه به علت طولانی بودن زمان نصب آن، بلکه به علت وجود نکات ریزی که در هیچ مستنداتی، به صورت مدون پیدا نخواهید کرد و عدم آشنایی با آنها ممکن است سبب بروز حملهی قلبی، به علت در دست داشتن سخت افزاری شود که هم اکنون کل آنرا فرمت کردهاید و ... راهنماهای ارائه شدهی در اینترنت هم بر روی آن کار نمیکنند! به یک چنین سخت افزاری، brick یا «پاره آجر» هم گفته میشود!)
دریافت Custom ROM سازگار با گوشی یا تبلت
مرحلهی اول نصب LineageOS، دریافت Custom ROM آن است. برای این منظور به آدرس download.lineageos.org مراجعه کرده و ابتدا از منوی سمت چپ صفحه، گوشی خود را پیدا کنید و سپس با انتخاب آن، امکان دریافت ROM مخصوص آنرا خواهید یافت.
نکتهی مهم! متاسفانه در اولین دریافت من از این سایت، به علت ناقص بودن دانلود، فایل دریافتی به همراه CRC Error بود و در زمان نصب فایل zip آن، خطای کلی e1001 ظاهر شد و نه هیچ چیز دیگری. این لحظه واقعا لحظهای است که ممکن است عرق سرد بر روی پیشانی شما ظاهر شود! به صورت اتفاقی با بررسی فایل zip آن بر روی کامپیوتر متوجه شدم که فایل، ناقص دریافت شده. به همین جهت پیش از شروع به نصب، فایل zip را در یک برنامهی باز کنندهی آنها مانند winrar و یا 7-zip باز کرده و بر روی دکمهی test آنها کلیک کنید. اگر خطایی را گزارش ندادند، شروع به ادامهی مراحل نصب کنید.
دریافت فایل Recovery سفارشی
در اینجا نیاز است با دو واژهی جدید bootloader و recovery آشنا شد. زمانیکه گوشی خودتان را روشن میکنید، اولین نرم افزاری که حتی پیش از سیستم عامل اجرا میشود، bootloader نام دارد که کار آن آغاز سایر پروسهها است. بعد از بارگذاری بوتلودر، برنامهی دیگری به نام recovery، کار بارگذاری سیستم عامل را انجام میدهد. بوتلودر و recovery پیشفرض اندروید، اجازهی نصب یک custom ROM را نمیدهند. به همین جهت نیاز است این برنامهی recovery را با یک نمونهی سفارشی بازنویسی کرد که این نمونهی سفارشی در اینجا TWRP نام دارد و نمونهی مخصوص گوشی خود را میتوانید با جستجوی در لیست https://twrp.me/Devices دریافت کنید. ابتدا نوع گوشی و سپس مدل آنرا یافته و سپس در صفحهای که ظاهر میشود، بر روی download link آن کلیک کنید تا لیست فایلهای موجود ظاهر شوند. در ابتدا آخرین نگارش موجود را دریافت کنید.
یک تجربه! متاسفانه آخرین نگارش TWRP دریافت شده، بر روی گوشی من کار نکرد و پس از نصب آن، مدام وارد همان سیستم عامل قبلی، با ارائهی پیام «Recovery is NOT SEANDROID Enforcing» میشد و هیچ تاثیری را نداشت. در این حالت نصب نگارش قدیمیتر 3.3.1، کار کرد. بنابراین بهتر است چندین نگارش آنرا دریافت کنید؛ تا در صورت لزوم بتوانید یکی یکی، آنها را آزمایش کنید.
دریافت Google Apps
LineageOS به همراه برنامههای گوگل، مانند play store و امثال اینها نیست. به همین جهت نیاز است آنها را از آدرس https://opengapps.org دریافت کنید. در اینجا دقت داشته باشید که چه چیزی را انتخاب میکنید! برای نمونه برای گوشی من گزینههای ARM، نگارش 10 و pico انتخاب شدند و سپس کلیک بر روی دکمهی دانلود. گزینهی pico، یکی از کم حجمترین نگارشها است و همینقدر برای شروع به کار، کافی است. نگارش را 10 انتخاب میکنیم چون میخواهیم اندروید 10 را نصب کنیم و انتخاب معماری CPU گوشی هم مهم است. با استفاده از برنامهای مانند device info، به برگهی CPU آن مراجعه کرده و CPU Type گوشی خود را دقیق بررسی کنید. اگر مانند گوشی من، 32bit بود، باید ARM را انتخاب کنید و اگر 64bit بود، گزینهی ARM64 را انتخاب کنید و اگر یک گوشی قدیمی را مانند ASUS دارید، ممکن است CPU آن از نوع intel و x86 باشد.
دریافت برنامهی فعالسازی دسترسی root
اگر میخواهید دسترسی root هم داشته باشید (این گزینه اختیاری است و من آنرا نصب نکردم)، در نگارشهای قبلی LineageOS از برنامهای به نام SU برای انجام اینکار استفاده میشد. این برنامه دیگر نگهداری نمیشود و نباید آنرا به همراه آخرین نگارش LineageOS نصب کرد (خیلی مهم!)؛ وگرنه گوشی شما را حتما به هم خواهد ریخت. برنامهی جایگزین آن Magisk نام دارد که باز هم من آنرا توصیه نمیکنم! چون اگر به انجمنهای LineageOS مراجعه کنید، مشاهده شدهاست که پس از نصب به روز رسانیهای جدید هفتگی LineageOS، ممکن است به علت عدم سازگاری با Magisk، سیستم عامل گوشی شما بالا نیاید و در یک حلقهی بیپایان قرار بگیرید. به همین جهت بهتر است از این گزینه صرفنظر کنید.
آماده سازی گوشی برای اتصال USB و اجرای فرامین بر روی آن
مرحلهی بعد، نصب برنامهی recovery سفارشی است. برای اینکار نیاز است گوشی خود را توسط سیم USB، به یک کامپیوتر متصل کرده و سپس توسط برنامهای خاص که در ادامه معرفی میشود، برنامهی TWRP را بر روی آن نصب کرد. به همین جهت به قسمت «تنظیمات» گوشی اندرویدی خود رفته و گزینهی «دربارهی دستگاه (About)» را پیدا کنید. سپس بر روی شمارهی build آن «Build Number»، هفت بار ضربه بزنید. اینکار سبب میشود تا یک منوی مخفی به نام «Developer Mode» یا «گزینههای توسعه دهندگان/برنامه نویسان»، به لیست منوهای تنظیمات سیستم عامل فعلی اضافه شود. پس از فعال شدن «Developer Mode»، به این گزینه وارد شده و دو گزینهی زیر را در آن فعال کنید:
- USB debugging
- OEM unlocking
اکنون اگر گوشی خود را از طریق سیم USB به کامپیوتر متصل کنید، یک دیالوگ باکس پرسشی، در اندروید جاری ظاهر میشود که درخواست دسترسی به ADB را از شما سؤال میپرسد. گزینهی «Always Allow From This Computer» را انتخاب کرده و با کلیک بر روی OK، این دسترسی را فعال کنید.
دریافت برنامههای انتقال اطلاعات به گوشی اندرویدی
پس از دریافت فایلهای مورد نیاز (TWRP.img, firmware.zip و gapps.zip)، اکنون نوبت به نصب TWRP.img است تا برنامهی recovery پیشفرض گوشی را با یک نمونهی سفارشی که امکان نصب custom ROM را میسر میکند، بازنویسی کنیم. بر روی گوشیهای سامسونگ، برنامهی ODIN یک چنین قابلیتی را به همراه دارد.
البته اگر کمی جستجو کنید، به دستورات زیر هم خواهید رسید که توسط برنامهی Minimal_ADB_Fastboot قابل اجرا هستند:
adb devices adb reboot bootloader fastboot devices fastboot flash recovery TWRP.img fastboot reboot-bootloader
البته برنامهی Minimal_ADB_Fastboot، برنامهی بسیار مفیدی است و در ادامه کاربردهایی از آنرا مطالعه خواهید کرد.
بررسی امنیتی مهم! آیا فایل ROM دریافت شده، بر روی گوشی من نصب میشود؟!
در ادامه پیش از نصب، یکبار گوشی را فرمت میکنیم. در این حال اگر در حین نصب، پیام سازگار نبودن فایل ROM را دریافت کنیم، بسیار دیر است! به همین جهت پس از نصب برنامهی Minimal_ADB_Fastboot، به پوشهی آن وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید (با این فرض که گوشی شما از طریق سیم USB به کامپیوتر متصل است و همچنین دسترسی دیباگی را هم که در گوشی عنوان شد، دادهاید):
adb devices adb shell getprop ro.product.device adb shell getprop ro.build.product
سپس فایل custom ROM دریافت شده را باز کرده و به پوشهی «META-INF\com\google\android» آن وارد شوید. در اینجا فایل متنی updater-script را باز کنید. برای مثال در مورد گوشی من، چنین سطری در ابتدای آن درج شده:
assert(getprop("ro.product.device") == "j7elte" || getprop("ro.build.product") == "j7elte" || abort("E3004: This package is for device: j7elte; this device is " + getprop("ro.product.device") + "."););
بنابراین حتما پیش از مطالعه و اجرای ادامهی بحث، مقادیر این ویژگیها را با سطر اول فایل updater-script انطباق دهید تا اگر یکی نبودند، به اشتباه گوشی خود را فرمت نکنید!
البته در جائی دیدم که عدهای برای «خوراندن» rom سفارشی دریافت شده، این سطر بررسی را از فایل یاد شده، پاک کرده و سپس فایل zip جدیدی را تولید و نصب کردهاند. بهتر است اینکار را نکنید و با جستجوی دقیق مطمئن شوید که یک چنین تغییری، برای سیستم شما مشکلی را ایجاد نمیکند!
بازنویسی برنامهی recovery گوشی توسط ODIN
پس از دریافت برنامهی odin، نیاز است گوشی خود را خاموش کنید. فرض بر این است که پیشتر حداقل از contacts خود پشتیبان تهیه کردهاید. چون از این قسمت به بعد، به مراحل بدون بازگشتی قدم خواهیم گذاشت و قرار است گوشی را کاملا فرمت کنیم!
پس از خاموش کردن گوشی، اکنون نیاز است گوشی را در حالت download بالا بیاوریم. برای اینکار سه دکمهی Volume Down + Home + Power با هم بفشارید. بنابراین ابتدا دکمهی «کاهش صدا» را نگه دارید و رها نکنید، سپس دکمهی home را نگه دارید و رها نکنید و در آخر دکمهی power را نگه دارید تا گوشی به حالت ویژهی download وارد شود.
البته در ابتدا یک صفحهی اخطار را نمایش میدهد که در آن درج شده برای ادامه نیاز است دکمهی «افزایش صدا» را بفشارید.
پس از ظاهر شدن تصویر فوق، اینبار دکمهی «افزایش صدا» را بفشارید تا وارد حالت دانلود شوید. در اینجا «حالت دانلود» یعنی گوشی قابلیت دریافت فایلی را پیدا کردهاست.
پس از بالا آوردن گوشی در حالت دانلود، برنامهی odin را باز کنید:
- در اینجا در قسمت AP، فایل tar مربوط به TWRP را انتخاب کنید.
- سپس در برگهی options، تیک گزینهی Auto reboot را بردارید (بسیار مهم!). اگر این تیک را برندارید، پس از کار نوشتن برنامهی recovery سفارشی، گوشی شما reboot شده و ... وارد برنامهی recovery ... نمیشود! چون سیستم امنیتی توکار اندروید، آنرا با نمونهی اصلی جایگزین میکند!
- اکنون بر روی دکمهی start کلیک کنید تا کار بازنویسی شروع شود.
پس از پایان بازنویسی برنامهی recovery، باید وارد این برنامهی جدید بشویم که روش ورود به آن به صورت زیر است:
پس از پایان بازنویسی، در بعضی از گوشیها در همین حالت که گوشی، حالت download را نمایش میدهد، اگر ترکیب کلیدهای «Volume Up + Power + Home» را بفشارید (اینبار دکمهی «افزایش صدا» است و نه کاهش صدا)، وارد این برنامهی recovery جدید میشوید. اما در مورد گوشی من چنین چیزی رخ نداد. در این حالت تنها روشی که پاسخ داد، «خارج کردن باطری گوشی» بود (در همین حالتی که صفحهی آبی رنگ download نمایش داده میشود، باطری را خارج کنید)؛ چون حتی در حالت خاموش کردن معمولی هم برنامهی recovery سفارشی را پاک و نمونهی اصلی را جایگزین میکرد!
سپس سیستم را به صورت معمولی روشن نکنید. اینبار نیاز است وارد منوی recovery شویم. بنابراین مجددا باطری را قرار داده و اینبار با فشردن ترکیب کلیدهای «Volume Up + Power + Home» به منوی جدید recovery وارد خواهیم شد.
مرحلهی آخر! نصب سیستم عامل جدید و برنامههای گوگل
تا اینجا باید وارد منوی recovery جدید شده باشید. روش خارج کردن باطری را هم فراموش نکنید! (چون اگر سیستم به صورت معمولی ریاستارت شود، یا حتی به صورت معمولی خاموش شود، برنامهی recovery سفارشی را بیاثر کرده و پاک میکند)
- پس از بارگذاری برنامه، پیام «Swipe to Allow Modifications» ظاهر میشود. برای این منظور، فلش آبی رنگ ظاهر شده را به سمت راست بکشید تا بتوانید وارد برنامه شوید.
- اکنون این مراحل را طی کنید:
الف) انتخاب Wipe
در اینجا در ابتدا گزینهی Format Data را انتخاب کنید.
سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار فرمت کردن سیستم شروع شود.
در ادامه در همین قسمت گزینهی Advanced Wipe را انتخاب کرده (همیشه با انتخاب دکمهی back میتوان به منوی اصلی و گزینههای آن رسید) و Dalvik / ART Cache,Data, System, Cache, Internal storage را انتخاب کنید. سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار پاک کردن سیستم شروع شود. در اینجا همه چیز را منهای SD Card، پاک خواهیم کرد. بدون انجام اینکار، وارد یک حلقهی بینهایت خواهید شد و سیستم اصلی پس از نصب، راه اندازی نمیشود (آزمایش کردم!).
ب) انتقال فایلهای Custom ROM و GApps به گوشی
اکنون به کامپیوتر خود و پوشهی محل نصب برنامهی Minimal_ADB_Fastboot وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید تا فایلها به گوشی منتقل شوند:
adb devices adb push LINEAGE.zip /sdcard/ adb push GAPPS.zip /sdcard/
1- فایلهای custom ROM و GApps دریافت شده را به درون پوشهی Minimal_ADB_Fastboot کپی کنید. در اینجا منظور از LINEAGE.zip نام کاملی مانند lineage-17.1-20210114-nightly-j7elte-signed.zip است که دریافت کردهاید و همچنین منظور از GAPPS.zip، نام کاملی مانند open_gapps-arm-10.0-pico-20210116.zip است.
2- برای اجرای این دستورات، نیازی به داشتن یک sdcard نیست. نامی که در اینجا ذکر شده، فقط یک نام پوشهی جدید، در گوشی شما است که قرار است در ادامه فایلها را از آن انتخاب کنیم.
یک نکتهی تکمیلی: در حالت منوی recovery و بعد از پاک کردن همه چیز، اگر فولدرهای گوشی در windows explorer مشخص نیستند، باید آنها را mount کرد تا بشود فایلها را به آنها کپی کرد (روش دوم کپی کردن فایلها به گوشی). اگر به منوی ابتدایی TWRP دقت کنید، یک گزینهی mount هم دارد که دقیقا برای همین منظور است. پوشهها را که mount کردید، در windows explorer جهت کپی کردن معمولی ظاهر میشوند.
ج) نصب نهایی سیستم عامل و برنامههای گوگل
پیش از هر کاری به گزینهی Settings در برنامهی TWRP مراجعه کرده و در برگهی General آن، تیک زیر را بر دارید: Prompt to install TWRP app if not installed!
اکنون که فایلهای custom ROM و GApps به گوشی کپی شدند، از منوی اصلی TWRP، اینبار گزینهی Install را انتخاب کنید (همانطور که عنوان شد، در اینجا همیشه دکمهی back، برای بازگشت به صفحهی اصلی کار میکند).
اگر از طریق دستورات adb فایلها را به پوشهی sdcard منتقل کرده باشید، به صورت خودکار اولین فایل انتخاب شده همان فایل ROM است. سپس بر روی دکمهی «Add more zips» کلیک کرده و فایل zip مربوط به GApps را انتخاب کنید. در بالای صفحه «two of max 10 File queued» باید ظاهر شده باشد (مهم) که به معنای تعداد فایلهای موجود در صف نصب است. اکنون فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار نصب شروع شود.
پس از پایان نصب این دو برنامه، یکبار بر روی دکمهی Wipe cache/dalvik کلیک کنید (به همراه به سمت راست کشیدن دکمهی فلش آبی پایین صفحه) و سپس بر روی دکمهی Reboot System تا ... وارد اندروید 10 شوید!
یک نکته: در اینجا در حین reboot سؤال میپرسد که آیا نیاز است TWRP را نیز به صورت جداگانهای نصب کند. عنوان کنید، خیر.
چگونه به روز رسانیهای LineageOS را نصب کنیم؟
LineageOS هفتهای یکبار، آخرین به روز رسانیهای اندروید را توزیع میکند. برای نصب آنها پیامی را ظاهر کرده و امکان دانلود را فراهم میکند. پس از دانلود، اگر بر روی دکمهی install کلیک کنید، به صورت خودکار شما را وارد منوی recovery فوق میکند (و نه نصب خودکار). در اینجا تنها کاری را که باید انجام دهید، انتخاب گزینهی install است و سپس انتخاب پوشهی data/lineageos_updates که محل قرار گیری این فایل zip دریافت شدهاست. با انتخاب فایل zip، مراحل نصب آن مانند قبل است. پس از پایان نصب، یکبار بر روی دکمهی پاک کردن کش dalvik کلیک کنید و سپس بر روی reboot. کش dalvik همواره به صورت خودکار توسط اندروید ساخته میشود و پاک کردن آن مشکلی را ایجاد نمیکند.
پس از راه اندازی مجدد سیستم، به منوی Settings>about phone>lineageOS مراجعه کرده و فایل zip قدیمی را حذف کنید (در همان صفحهای که پیام دریافت و نصب را نمایش میداد، اکنون پیام delete ظاهر شدهاست).
اشتراکها
Process Explorer v17.0 منتشر شد
نظرات مطالب
پیاده سازی UnitOfWork به وسیله MEF
من کلاسهام به این شکله:
کلاس کانتکسهای من
در تعاریف کلاسهایی که از IDBContext ارث میبرن اکسپورت شدن (این یک نمونه از کلاسهای منه)
در طرف دیگر برای لود کردن کلاس زیر نوشتم
و در کانتکس اصلی برنامه این پلاگین هارو لود میکنم
کلاس کانتکسهای من
public class VegaContext : DbContext, IUnitOfWork, IDbContext { #region Constructors (2) /// <summary> /// Initializes the <see cref="VegaContext" /> class. /// </summary> static VegaContext() { Database.SetInitializer<VegaContext>(null); } /// <summary> /// Initializes a new instance of the <see cref="VegaContext" /> class. /// </summary> public VegaContext() : base("LocalSqlServer") { } #endregion Constructors #region Properties (2) /// <summary> /// Gets or sets the languages. /// </summary> /// <value> /// The languages. /// </value> public DbSet<Language> Languages { get; set; } /// <summary> /// Gets or sets the resources. /// </summary> /// <value> /// The resources. /// </value> public DbSet<Resource> Resources { get; set; } #endregion Properties #region Methods (2) // Public Methods (1) /// <summary> /// Setups the specified model builder. /// </summary> /// <param name="modelBuilder">The model builder.</param> public void Setup(DbModelBuilder modelBuilder) { //todo modelBuilder.Configurations.Add(new ResourceMap()); modelBuilder.Configurations.Add(new LanguageMap()); modelBuilder.Entity<Resource>().ToTable("Vega_Languages_Resources"); modelBuilder.Entity<Language>().ToTable("Vega_Languages_Languages"); //base.OnModelCreating(modelBuilder); } // Protected Methods (1) /// <summary> /// This method is called when the model for a derived context has been initialized, but /// before the model has been locked down and used to initialize the context. The default /// implementation of this method does nothing, but it can be overridden in a derived class /// such that the model can be further configured before it is locked down. /// </summary> /// <param name="modelBuilder">The builder that defines the model for the context being created.</param> /// <remarks> /// Typically, this method is called only once when the first instance of a derived context /// is created. The model for that context is then cached and is for all further instances of /// the context in the app domain. This caching can be disabled by setting the ModelCaching /// property on the given ModelBuidler, but note that this can seriously degrade performance. /// More control over caching is provided through use of the DbModelBuilder and DbContextFactory /// classes directly. /// </remarks> protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new ResourceMap()); modelBuilder.Configurations.Add(new LanguageMap()); modelBuilder.Entity<Resource>().ToTable("Vega_Languages_Resources"); modelBuilder.Entity<Language>().ToTable("Vega_Languages_Languages"); base.OnModelCreating(modelBuilder); } #endregion Methods #region IUnitOfWork Members /// <summary> /// Sets this instance. /// </summary> /// <typeparam name="TEntity">The type of the entity.</typeparam> /// <returns></returns> public new IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } #endregion }
در طرف دیگر برای لود کردن کلاس زیر نوشتم
public class LoadContexts { public LoadContexts() { var directoryPath = HttpRuntime.BinDirectory;//AppDomain.CurrentDomain.BaseDirectory; //"Dll folder path"; var directoryCatalog = new DirectoryCatalog(directoryPath, "*.dll"); var aggregateCatalog = new AggregateCatalog(); aggregateCatalog.Catalogs.Add(directoryCatalog); var container = new CompositionContainer(aggregateCatalog); container.ComposeParts(this); } //[Import] //public IPlugin Plugin { get; set; } [ImportMany] public IEnumerable<IDbContext> Contexts { get; set; } }
public class MainContext : DbContext, IUnitOfWork { public MainContext() : base("LocalSqlServer") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var contextList = new LoadContexts(); //ObjectFactory.GetAllInstances<IDbContext>(); foreach (var context in contextList.Contexts) context.Setup(modelBuilder); Database.SetInitializer(new MigrateDatabaseToLatestVersion<MainContext, Configuration>()); //Database.SetInitializer(new DropCreateDatabaseAlways<MainContext>()); } /// <summary> /// Sets this instance. /// </summary> /// <typeparam name="TEntity">The type of the entity.</typeparam> /// <returns></returns> public IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } }
با موفقیت همه پلاگینها لود میشه و مشکلی در عملیات نیست. اما Attributeهای کلاس هارو نمیشناسه. مثلا پیام خطا تعریف شده در MVC نمایش داده نمیشه چون وجود نداره ولی وقتی کلاس مورد نظر از IValidatableObject ارث میبره خطایهای من نمایش داده میشه. میخوام از خود متادیتاهای استاندارد استفاده کنم.
The Open XML SDK provides tools for working with Office Word, Excel, and PowerPoint documents. It supports scenarios such as:
- High-performance generation of word-processing documents, spreadsheets, and presentations.
- Document modification, such as adding, updating, and removing content and metadata.
- Search and replace content using regular expressions.
- Splitting up (shredding) a file into multiple files, and combining multiple files into a single file.
- Updating cached data and embedded spreadsheets for charts in Word/PowerPoint.
در قسمت اول این سری، با مدل برنامه نویسی Event based asynchronous pattern ارائه شده از دات نت 2 و همچنین APM یا Asynchronous programming model موجود از نگارش یک دات نت، آشنا شدیم (به آن الگوی IAsyncResult هم گفته میشود). نکتهی مهم این الگوها، استفادهی گسترده از آنها در کدهای کلاسهای مختلف دات نت فریم ورک است و برای بسیاری از آنها هنوز async API سازگار با نگارش مبتنی بر Taskهای سیشارپ 5 ارائه نشدهاست. هرچند دات نت 4.5 سعی کردهاست این خلاء را پوشش دهد، برای مثال متد الحاقی DownloadStringTaskAsync را به کلاس WebClient اضافه کردهاست و امثال آن، اما هنوز بسیاری از کلاسهای دیگر دات نتی هستند که معادل Task based API ایی برای آنها طراحی نشدهاست. در ادامه قصد داریم بررسی کنیم چگونه میتوان این الگوهای مختلف قدیمی برنامه نویسی غیرهمزمان را با استفاده از روشهای جدیدتر ارائه شده بکار برد.
نگاشت APM به یک Task
در قسمت اول، نمونه مثالی را از APM، که در آن کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید، مشاهده کردید. در ادامه میخواهیم یک محصور کنندهی جدید را برای این نوع API قدیمی تهیه کنیم، تا آنرا به صورت یک Task ارائه دهد.
همانطور که در این مثال مشاهده میکنید، یک چنین سناریوهایی در TPL یا کتابخانهی Task parallel library پیش بینی شدهاند. در اینجا یک محصور کننده برای متدهای BeginRead و EndRead کلاس Stream دات نت ارائه شدهاست. به عمد نیز به صورت یک متد الحاقی تهیه شدهاست تا در حین استفاده از آن اینطور به نظر برسد که واقعا کلاس Stream دارای یک چنین متد Async ایی است. مابقی کار توسط متد Task.Factory.FromAsync انجام میشود. متد FromAsync دارای امضاهای متعددی است تا اکثر حالات APM را پوشش دهد.
در مثال فوق BeginRead و EndRead استفاده شده از نوع delegate هستند. چون خروجی EndRead از نوع int است، خروجی متد نیز از نوع Task of int تعیین شدهاست. همچنین سه پارامتر ابتدایی BeginRead ، دقیقا data، offset و count هستند. دو پارامتر آخر آن callback و state نام دارند. پارامتر callback توسط متد FromAsync فراهم میشود و state نیز در اینجا null درنظر گرفته شدهاست.
یک مثال استفاده از آنرا در ادامه مشاهده میکنید:
File.OpenRead، خروجی از نوع استریم دارد. سپس متد الحاقی ReadAsync بر روی آن فراخوانی شدهاست و نهایتا تعداد بایت خوانده شده نمایش داده میشود.
البته همانطور که پیشتر نیز عنوان شد، استفاده از خاصیت Result، اجرای کد را بجای غیرهمزمان بودن، به حالت همزمان تبدیل میکند.
در اینجا چون خروجی متد ReadAsync یک Task است، میتوان از متد ContinueWith نیز بر روی آن جهت دریافت نتیجه استفاده کرد:
یک نکته
پروژهی سورس بازی به نام Async Generator در GitHub، سعی کردهاست برای ساده سازی نوشتن محصور کنندههای مبتنی بر Task روش APM، یک Code generator تولید کند. فایلهای آنرا از آدرس ذیل میتوانید دریافت کنید:
نگاشت EAP به یک Task
نمونهای از Event based asynchronous pattern یا EAP را در قسمت اول، زمانیکه روال رخدادگردان webClient.DownloadStringCompleted را بررسی کردیم، مشاهده نمودید. کار کردن با آن نسبت به APM بسیار سادهتر است و نتیجهی نهایی عملیات غیرهمزمان را در یک روال رخدادگران، در اختیار استفاده کننده قرار میدهد. همچنین در روش EAP، اطلاعات در همان Synchronization Context ایی که عملیات شروع شدهاست، بازگشت داده میشود. به این ترتیب اگر آغاز کار در ترد UI باشد، نتیجه نیز در همان ترد دریافت خواهد شد. به این ترتیب دیگر نگران دسترسی به مقدار آن در کارهای UI نخواهیم بود؛ اما در APM چنین ضمانتی وجود ندارد.
متاسفانه TPL همانند روش FromAsync معرفی شده در ابتدای بحث، راه حل توکاری را برای محصور سازی متدهای روش EAP ارائه ندادهاست. اما با استفاده از امکانات TaskCompletionSource آن میتوان چنین کاری را انجام داد. در ادامه سعی خواهیم کرد همان متد الحاقی توکار DownloadStringTaskAsync ارائه شده در دات نت 4.5 را از صفر بازنویسی کنیم.
روش انجام کار را در اینجا ملاحظه میکنید. ابتدا باید تعاریف delaget مرتبط با رخدادگردان Completed اضافه شوند. یکبار += را ملاحظه میکنید و بار دوم -= را. مورد دوم جهت آزاد سازی منابع و جلوگیری از نشتی حافظهی روال رخدادگردان هنوز متصل، ضروری است.
سپس از TaskCompletionSource برای تبدیل این عملیات به یک Task کمک میگیریم. اگر args.Cancelled مساوی true باشد، یعنی عملیات دریافت فایل لغو شدهاست. بنابراین متد SetCanceled منبع Task ایجاد شده را فراخوانی خواهیم کرد. این مورد استثنایی را در کدهای فراخوان سبب میشود. به همین دلیل بررسی خطا با یک if else پس از آن انجام شدهاست. برای بازگشت خطای دریافت شده از متد SetException و برای بازگشت نتیجهی واقعی دریافتی، از متد SetResult میتوان استفاده کرد.
به این ترتیب متد الحاقی غیرهمزمان جدیدی را به نام DownloadTextTaskAsync برای محصور سازی متد EAP ایی به نام DownloadStringAsync و همچنین رخدادگران آن تهیه کردیم.
نگاشت APM به یک Task
در قسمت اول، نمونه مثالی را از APM، که در آن کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجهی عملیات به دست میآید، مشاهده کردید. در ادامه میخواهیم یک محصور کنندهی جدید را برای این نوع API قدیمی تهیه کنیم، تا آنرا به صورت یک Task ارائه دهد.
public static class ApmWrapper { public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count) { return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null); } }
در مثال فوق BeginRead و EndRead استفاده شده از نوع delegate هستند. چون خروجی EndRead از نوع int است، خروجی متد نیز از نوع Task of int تعیین شدهاست. همچنین سه پارامتر ابتدایی BeginRead ، دقیقا data، offset و count هستند. دو پارامتر آخر آن callback و state نام دارند. پارامتر callback توسط متد FromAsync فراهم میشود و state نیز در اینجا null درنظر گرفته شدهاست.
یک مثال استفاده از آنرا در ادامه مشاهده میکنید:
using System; using System.IO; using System.Threading.Tasks; namespace Async06 { public static class ApmWrapper { public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count) { return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null); } } class Program { static void Main(string[] args) { using (var stream = File.OpenRead(@"..\..\program.cs")) { var data = new byte[10000]; var task = stream.ReadAsync(data, 0, data.Length); Console.WriteLine("Read bytes: {0}", task.Result); } } } }
البته همانطور که پیشتر نیز عنوان شد، استفاده از خاصیت Result، اجرای کد را بجای غیرهمزمان بودن، به حالت همزمان تبدیل میکند.
در اینجا چون خروجی متد ReadAsync یک Task است، میتوان از متد ContinueWith نیز بر روی آن جهت دریافت نتیجه استفاده کرد:
using (var stream = File.OpenRead(@"..\..\program.cs")) { var data = new byte[10000]; var task = stream.ReadAsync(data, 0, data.Length); task.ContinueWith(t => Console.WriteLine("Read bytes: {0}", t.Result)).Wait(); }
یک نکته
پروژهی سورس بازی به نام Async Generator در GitHub، سعی کردهاست برای ساده سازی نوشتن محصور کنندههای مبتنی بر Task روش APM، یک Code generator تولید کند. فایلهای آنرا از آدرس ذیل میتوانید دریافت کنید:
نگاشت EAP به یک Task
نمونهای از Event based asynchronous pattern یا EAP را در قسمت اول، زمانیکه روال رخدادگردان webClient.DownloadStringCompleted را بررسی کردیم، مشاهده نمودید. کار کردن با آن نسبت به APM بسیار سادهتر است و نتیجهی نهایی عملیات غیرهمزمان را در یک روال رخدادگران، در اختیار استفاده کننده قرار میدهد. همچنین در روش EAP، اطلاعات در همان Synchronization Context ایی که عملیات شروع شدهاست، بازگشت داده میشود. به این ترتیب اگر آغاز کار در ترد UI باشد، نتیجه نیز در همان ترد دریافت خواهد شد. به این ترتیب دیگر نگران دسترسی به مقدار آن در کارهای UI نخواهیم بود؛ اما در APM چنین ضمانتی وجود ندارد.
متاسفانه TPL همانند روش FromAsync معرفی شده در ابتدای بحث، راه حل توکاری را برای محصور سازی متدهای روش EAP ارائه ندادهاست. اما با استفاده از امکانات TaskCompletionSource آن میتوان چنین کاری را انجام داد. در ادامه سعی خواهیم کرد همان متد الحاقی توکار DownloadStringTaskAsync ارائه شده در دات نت 4.5 را از صفر بازنویسی کنیم.
public static class WebClientExtensions { public static Task<string> DownloadTextTaskAsync(this WebClient web, string url) { var tcs = new TaskCompletionSource<string>(); DownloadStringCompletedEventHandler handler = null; handler = (sender, args) => { web.DownloadStringCompleted -= handler; if (args.Cancelled) { tcs.SetCanceled(); } else if(args.Error!=null) { tcs.SetException(args.Error); } else { tcs.SetResult(args.Result); } }; web.DownloadStringCompleted += handler; web.DownloadStringAsync(new Uri(url)); return tcs.Task; } }
سپس از TaskCompletionSource برای تبدیل این عملیات به یک Task کمک میگیریم. اگر args.Cancelled مساوی true باشد، یعنی عملیات دریافت فایل لغو شدهاست. بنابراین متد SetCanceled منبع Task ایجاد شده را فراخوانی خواهیم کرد. این مورد استثنایی را در کدهای فراخوان سبب میشود. به همین دلیل بررسی خطا با یک if else پس از آن انجام شدهاست. برای بازگشت خطای دریافت شده از متد SetException و برای بازگشت نتیجهی واقعی دریافتی، از متد SetResult میتوان استفاده کرد.
به این ترتیب متد الحاقی غیرهمزمان جدیدی را به نام DownloadTextTaskAsync برای محصور سازی متد EAP ایی به نام DownloadStringAsync و همچنین رخدادگران آن تهیه کردیم.