پیشنیازهای نصب Docker بر روی ویندوز
مطابق مستندات آن، برای نصب داکر بر روی ویندوز به حداقلهای زیر نیاز است:
- استفاده از ویندوز 10 نگارش enterprise، که شماره نگارش آن حداقل 1607 باشد (حداقل Anniversary Update باشد).
- مجازی سازی در BIOS فعال شده باشد.
البته مجازی سازی عموما به صورت پیشفرض فعال است. برای بررسی آن، taskmanager ویندوز را اجرا کرده و در برگهی Performance آن، جائیکه مشخصات CPU را نمایش میدهد، یک سطر به Virtualization اختصاص دارد که مقدار آن باید enabled باشد (تصویر زیر) و اگر نیست، برای فعال کردن آن باید به تنظیمات BIOS سیستم خود مراجعه کنید:
روش دیگر دریافت این اطلاعات، اجرای دستور systeminfo در خط فرمان، با دسترسی مدیریتی است. در خروجی آن، عبارت «Virtualization Enabled In Firmware» را جستجو کنید که باید مقدار آن yes باشد.
- داشتن CPU با قابلیت SLAT یا Second Level Address Translation.
برای یافتن این موضوع، برنامهی coreinfo را دریافت کرده و آنرا به صورت coreinfo -v اجرا کنید. خروجی آن سه سطر مرتبط با مجازی سازی را به همراه دارد. اگر قابلیتی موجود نباشد، جلوی آن یک خط تیره و اگر قابلیتی موجود باشد، روبروی آن یک ستاره را مشاهده خواهید کرد.
روش دیگر بررسی آن، اجرای دستور msinfo32 در قسمت run ویندوز و سپس enter است. در قسمت system summary، اطلاعات Second Level Address Translation قابل مشاهده هستند (اگر No باشد، امکان اجرای containerهای لینوکسی را بر روی ویندوز نخواهید داشت):
- داشتن حداقل 4 گیگابایت RAM.
- فعال بودن Hyper-V نیز برای اجرای Linux Containers بر روی ویندوز، ضروری است (نصاب Docker، اینکار را به صورت خودکار انجام میدهد).
دریافت نصاب Docker for Windows
برای دریافت نصاب داکر مخصوص ویندوز، به آدرس زیر مراجعه کنید:
https://store.docker.com/editions/community/docker-ce-desktop-windows
که بلافاصله با تصویر کریه زیر مواجه خواهید شد:
برای رفع این مشکل، میتوان از روش مطرح شدهی در مطلب «یک روش ساده برای دور زدن تحریمها!» استفاده کرد؛ یعنی تنظیم DNS به 178.22.122.100 به صورت زیر:
پس از این تغییر، چون IP قابل مشاهدهی سیستم شما توسط سایت داکر تغییر میکند، اینبار صفحهی دریافت Docker Community Edition for Windows به صورت زیر ظاهر میشود:
همانطور که مشاهده میکنید، عنوان کردهاست که لطفا لاگین کنید تا بتوانید این برنامه را دریافت کنید. به همین جهت بر روی لینک آن کلیک کرده، یک اکانت جدید را در سایت docker ایجاد کنید (با یک ایمیل واقعی که تائیدیه آنرا دریافت خواهید کرد). پس از آن، با این اکانت جدید به سایت داکر وارد شوید تا لینک دریافت فایل exe نصاب آنرا دریافت کنید.
در این حالت مرورگر و یا حتی دانلودمنیجر شما بدون مشکل میتوانند این فایل را دریافت کنند و همان تنظیم DNS فوق، مشکل عدم دسترسی را برطرف میکند.
نصب Docker for Windows
پس از اجرای نصاب آن و پایان عملیات نصب (که تنها کافی است در صفحهی ابتدایی آن تیک مربوط به Windows Containers را نیز قرار دهید)، نیاز دارد تا شما را یکبار از سیستم Logout و login کند. پس از ورود به سیستم، تنظیمات ابتدایی آن به صورت خودکار صورت گرفته و در صورت فعال نبودن Hyper-V، پیام زیر را مشاهده خواهید کرد:
بر روی OK کلیک کنید تا اینکار با موفقیت به پایان برسد. البته پس از آن، منتظر حداقل یکبار ریاستارت شدن خودکار سیستم، بدون اطلاع قبلی نیز باشید.
یک نکته: کاری که در قسمت فعالسازی Hyper-V به صورت خودکار انجام میشود، شامل اجرای سه دستور زیر، در کنسول پاور شل، با دسترسی مدیریتی و سپس ری استارت سیستم است:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -Verbose Enable-WindowsOptionalFeature -Online -FeatureName Containers -All -Verbose bcdedit /set hypervisorlaunchtype Auto
C:\Users\Vahid>docker info Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 Server Version: 18.06.1-ce OSType: windows
بررسی تنظیمات سوئیچ کردن بین Linux Containers و Windows Containers
پس از اتمام ریاستارتها، برای آزمایش فعال بودن Hyper-V، در قسمت Run ویندوز، عبارت Virtmgmt.msc را نوشته و enter کنید. اگر تصویر زیر را مشاهده نمیکنید:
یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Linux containers را انتخاب کنید تا پس از مدتی، آیکن MobyLinuxVM در قسمت virtual machines (تصویر فوق) ظاهر شود.
اگر پس از انتخاب این گزینه، پیام زیر را دریافت کردید:
و یا اگر بر روی این ماشین مجازی کلیک راست کردید و گزینهی Start آنرا انتخاب کردید و پیام زیر ظاهر شد:
قسمت «پیشنیازهای نصب Docker بر روی ویندوز» را با دقت بررسی کنید (خصوصا قسمت BIOS و SLAT). نبود یکی از موارد ذکر شده، سبب بروز این مشکل میشود.
برای مثال اجرای دستور coreinfo -v بر روی سیستم من چنین خروجی را به همراه دارد:
E:\>coreinfo -v AuthenticAMD Microcode signature: 00000000 HYPERVISOR - Hypervisor is present SVM * Supports AMD hardware-assisted virtualization NP - Supports AMD nested page tables (SLAT)
همانطور که مشاهده میکنید، قابلیت SLAT در CPU این سیستم وجود ندارد. به همین جهت نمیتوان به Linux containers سوئیچ کرد. هرچند windows containers آن کار میکند.
روش دیگر مشاهدهی این خطا، مراجعهی به event viewer ویندوز است. در قسمت خطاهای سیستم، ممکن است خطای زیر را مشاهده کنید:
Hypervisor launch failed; Second Level Address Translation is required to launch the hypervisor.
آزمایش Docker نصب شده
پس از نصب docker، خط فرمان ویندوز را گشوده و دستور زیر را صادر کنید:
docker run hello-world
یک نکته: این image، یک image لینوکسی است. به همین جهت پیش از اجرای این دستور، همانطور که پیشتر نیز عنوان شد، یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Linux containers را انتخاب کنید. سپس دستور docker run hello-world را اجرا نمائید.
و یا در همین حال دستور docker run -p 80:80 nginx را صادر کنید تا وب سرور لینوکسی nginx را بتوانید تحت ویندوز اجرا کنید. پس از خاتمهی عملیات دریافت و اجرای وب سرور، با توجه به تنظیم p 80:80-، پورت 80 میزبان (اولین عدد)، به پورت 80 کانتینر نگاشت شدهاست. به همین جهت تنها با اجرای دستور http://localhost، خروجی این وب سرور را میتوانید در مرورگر سیستم خود مشاهده کنید.
همانطور که مشاهده میکنید، با استفاده از داکر، پیش از آنکه بدانیم چگونه باید یک نرم افزار را نصب کرد، میتوان از آن استفاده کرد!
روش متوقف کردن Containers در حال اجرا
اگر دستور docker ps را در خط فرمان ویندوز اجرا کنید، لیست پروسههای اجرا شدهی توسط آن قابل مشاهده هستند. در این لیست container id در حال اجرا نیز مشخص است. برای خاتمهی کار آن، تنها کافی است دستور docker stop id را اجرا کنید.
یک نکته: ضرورتی به ذکر کامل id نیست. برای مثال ذکر سه حرف اول آن نیز کفایت میکند.
روش اجرای مجدد یک Container
فرض کنید میخواهیم سرور nginx را مجددا اجرا کنیم. یک روش آن، اجرای مجدد دستور docker run -p 80:80 nginx است که پیشتر آنرا انجام دادیم. در این حالت این image تبدیل به container شده و همانند روشهای متداول نصب نرم افزار، اکنون به عنوان یک نرم افزار نصب شده در دسترس است. برای مشاهدهی لیست آنها، دستور docker ps -a را اجرا کنید. این لیست تا این لحظه باید شامل containerهای nginx و hello-world باشد. متوقف کردن یک container، سبب تخریب یا حذف آن نمیشود. در این حالت در لیستی که توسط دستور docker ps -a نمایش داده شدهاست، باز هم container idها قابل مشاهده هستند. فقط کافی است برای اجرای یکی از آنها، دستور docker start id را اجرا کرد. به این صورت دیگر نیازی به ذکر دستور کامل docker run با تمام پارامترهای آن نیست. این id نیز همانطور که ذکر شد، میتواند سه حرف ابتدایی این id باشد تا حدی که نسبت به سایر idهای موجود، منحصربفرد شناخته شود. یا بجای container id میتوان container name نمایش داده شدهی در این لیست را استفاده کرد.
پس از اجرای nginx توسط دستور docker start id، دو روش برای بررسی در حال اجرا بودن آن وجود دارد:
الف) مرورگر را باز کنیم و آدرس http://localhost را بررسی کنیم.
ب) دستور docker ps را در خط فرمان اجرا کنیم، تا مشخص شود که آیا پروسهی nginx در حال اجرا است یا خیر؟
بنابراین دستور docker ps -a لیست تمام containers در حال اجرا و همچنین متوقف شده را نمایش میدهد. اما دستور docker ps تنها لیست containers در حال اجرا را نمایش خواهد داد.
روش حذف Containers از Docker
همانطور که در قسمت قبل نیز بحث شد، معادل نصب نرم افزار در اینجا، ایجاد یک container از یک image دریافتی از docker hub است. روش عکس آن، یعنی تخریب یک container، دقیقا معادل عزل نرم افزار از سیستم، در حالتهای متداول است. برای اینکار مجددا دستور docker ps -a را اجرا میکنیم تا لیست تمام containerهای در حال اجرا و همچنین متوقف شده نمایش داده شوند. لیستی که در اینجا نمایش داده میشود، شبیه به لیستی است که در قسمت add/remove programs ویندوز مشاهده میکنید. این لیست معادل لیست نرم افزارهای نصب شدهی بر روی سیستم است و یا برای مشاهدهی لیست imageهای دریافتی از docker hub میتوان دستور docker images را صادر کرد.
قبل از حذف یک container نیاز است آنرا متوقف کنیم. برای این منظور از دستور docker stop id استفاده میشود. سپس اجرای دستور docker rm id، سبب حذف کامل این container خواهد شد. برای آزمایش آن، مجددا دستور docker ps -a را اجرا کنید.
دستور docker rm چندین id را نیز میپذیرد. میتوان این idها و یا حتی سه حرف ابتدایی آنها را با فاصله در اینجا ذکر کرد. علاوه بر id، ذکر نام containers نیز مجاز است.
روش حذف Imageهای دریافتی از Docker Hub
دستور docker rm، فقط containers را از سیستم حذف میکند (نرم افزارهای نصب شده). اما خود imageهای اصلی دریافت شدهی از docker hub را حذف نمیکند (معادل همان فایلهای zip دریافت نرم افزار یا برنامههای نصاب، در حالت متداول و سنتی نصب نرم افزار). برای آزمایش آن دستور docker images را اجرا کنید. هنوز هم در لیست آن، تمام موارد دریافتی موجود هستند.
برای حذف یک image میتوان از دستور docker rmi id استفاده کرد (rmi بجای rm). این id نیز در لیست docker images ظاهر میشود و ذکر قسمتی از آن، تا حدی که نسبت به سایر idهای لیست شده منحصربفرد باشد، کافی است. در اینجا بجای id، از نام image نیز میتوان استفاده کرد. همچنین ذکر چندین id و یا نام نیز پس از دستور docker rmi، میسر است.
روش جستجوی imageها در Docker Hub توسط Docker CLI
فرض کنید میخواهیم image مربوط به راهنمای Docker را از Docker Hub دریافت کنیم. یک روش آن مراجعهی مستقیم به سایت آن است و استفاده از امکانات جستجوی فراهم شدهی در آن سایت. روش دیگر، استفاده از Docker CLI است. اگر دستور docker search docs را در خط فرمان اجرا کنیم، لیست تمام مخازن کدی که در آنها واژهی docs قرار دارد، نمایش داده میشود. البته پیش از نصب image آن بهتر است به برگهی tags مخزن کد آن نیز مراجعه کنید تا بتوانید حجم آنرا نیز مشاهده نمائید که حدود یک گیگابایت است. مخازن docker hub، حاوی imageهای نصاب containerهای متناظر هستند. برای دریافت و اجرای آن میتوان دستور docker run -p 4000:4000 docs/docker.github.io را اجرا کرد.
پس از دریافت یک گیگابایت مستندات، container آن بر روی پورت 4000 در سیستم ما (http://localhost:4000)، به صورت یک وب سایت استاتیک، قابل دسترسی خواهد بود. به این صورت میتوان به مستندات کامل داکر به صورت آفلاین دسترسی داشت.
مفهوم Interactive Terminal در Docker
زمانیکه دستور اجرای مستندات آفلاین را صادر میکنید، در انتهای آن عنوان میکند که وب سایت محلی آن بر روی پورت 4000 قابل دسترسی است. سپس در ذیل آن ذکر شدهاست که اگر ctrl+c را فشار دهید، اجرای آن به پایان میرسد. اما عملا اینطور نیست و اگر دستور docker ps را صادر کنید، هنوز container در حال اجرای آن را میتوان مشاهده کرد.
اما اگر اینبار دستور اجرای docker run را به همراه یک interactive terminal با سوئیچ it و نام docs صادر کنیم:
docker run -p 4000:4000 -it --name docs docs/docker.github.io
سوئیچ it یا interactive terminal سبب میشود تا یک container در foreground، بجای background اجرا شود. به این ترتیب دستور ctrl+c، سبب خاتمهی واقعی پروسهی درحال اجرای در container میشود.
روش دیگر خاتمهی این container، استفاده از نام ذکر شدهاست؛ یعنی اجرای دستور docker stop docs.
یک نکته: اگر میخواهید از terminal باز شده قطع شوید (مجددا به command prompt باز گردید) اما سبب خاتمهی container آن نشوید، از ترکیب ctrl+p+q استفاده کنید.
اجرای containerهای ویندوزی
در مورد نحوهی سوئیچ بین نوعهای مختلف containerهای ویندوزی و لینوکسی پیشتر توضیح دادیم. برای این منظور میتوان بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینهی switch to Windows/Linux containers را انتخاب کرد. باید دقت داشت که پشتیبانی از containerهای ویندوزی، از ویندوز 10، نگارش 1607، یا همان Anniversary Update آن به بعد، به ویژگیهای ویندوز اضافه شدهاند که به صورت خودکار توسط docker فعالسازی میشوند:
اجرای IIS به عنوان یک Windows Container
تا اینجا imageهای دریافتی، لینوکسی بودند. اگر گزینهی Windows Containers را به روشی که گفته شد، فعال کنید، اینبار با اجرای دستورات docker ps و یا docker images، هیچ خروجی را دریافت نخواهید کرد. از این جهت که کانتینرهای ویندوزی و لینوکسی، به صورت کاملا ایزولهای از هم اجرا و مدیریت میشوند. علت آنرا هم در MobyLinuxVM که پیشتر با اجرای دستور Virtmgmt.msc بررسی کردیم، میتوان یافت. Containerهای لینوکسی، در داخل MobyLinuxVM اجرا میشوند.
در اینجا به عنوان مثال میتوان image رسمی مربوط به IIS را از docker hub دریافت و به صورت یک کانتینر ویندوزی اجرا کرد. البته پیش از اجرای دستورات آن بهتر است به برگهی tags آن مراجعه کرده و حجمهای نگارشهای مختلف آنرا بررسی کرد. اجرای دستور docker pull microsoft/iis به معنای دریافت tag ای به نام latest است (به حجم 6 گیگابایت!)؛ یعنی با دستور docker pull microsoft/iis:latest یکی است. بنابراین در اینجا بر اساس tagهای مختلف، میتوان دستور pull متفاوتی را صادر کرد. برای مثال اگر دستور docker pull microsoft/iis:nanoserver را صادر کردید، نگارش مخصوص nano server آنرا که فقط 449 مگابایت است، دریافت میکند. بنابراین از این پس به tagهای هر مخزن docker hub خوب دقت کنید و نگارش مختص به سیستم عامل خود را دریافت نمائید. عدم ذکر tag ای، همواره tag ویژهای را به نام latest، دریافت میکند.
با اجرای دستور زیر
docker run -p 81:80 -d --name iis microsoft/iis:nanoserver
یک نکته: مشکلی با اجرای IIS مخصوص نانوسرور بر روی ویندوز 10 به این صورت و توسط داکر نیست. بنابراین پس از اجرای دستور فوق، کار دریافت image و ساخت container و سپس اجرای آن به صورت خودکار انجام شده و بلافاصله به command prompt بازگشت داده میشویم (به علت استفادهی از پارامتر d). اکنون اگر دستور docker ps را صادر کنیم، مشاهده میکنیم که کانتینر IIS مخصوص نانوسرور، هم اکنون بر روی ویندوز 10 در حال اجرا است و در آدرس http://localhost:81 قابل دسترسی است.
جهت تکمیل این بحث، بهتر است image مخصوص nanoserver را نیز از docker hub دریافت و اجرا کنیم:
docker run microsoft/windowsservercore
تنظیمات کارت شبکهی Containers
هنگامیکه پروسهای درون یک container اجرا میشود، ایزوله سازیهای بسیاری نیز در مورد آن اعمال خواهد شد؛ به همین جهت گاهی از اوقات عدهای containerها را با ماشینهای مجازی نیز مقایسه میکنند. برای مثال کانتینرها به همراه network adapter خاص خود نیز هستند؛ درست مانند اینکه یک کامپیوتر مجزای از سیستم جاری میباشند و اگر این network adapter را ping کنیم، میتوان به این صورت نیز به آن کانتینر، دسترسی داشته باشیم.
برای یافتن آن، دستور docker inspect iis را صادر میکنیم. خروجی آن به همراه یک قسمت network نیز هست که داخل آن یک IP Address قابل مشاهده است. این IP است که مختص و منحصربفرد این container است. در ابتدا برای آزمایش آن، میتوان آنرا ping کرد؛ مانند ping 172.27.49.47. همچنین به تمام برنامههای داخل این container توسط این IP نیز میتوان دسترسی یافت. برای مثال فراخوانی http://172.27.49.47:81 در مرورگر، سبب نمایش صفحهی اول IIS میشود. البته اگر اینکار را انجام دهیم، کار نمیکند. علت اینجا است، نگاشت پورتی را که تعریف کردهایم (پورت 81)، به پورتی در کامپیوتر میزبان است و نه این IP ویژه. برنامهی اصلی IIS در داخل container، به پورت 80 بر روی این آدرس IP گوش فرا میدهد. اکنون اگر آدرس http://172.27.49.47:80 را در کامپیوتر میزبان فراخوانی کنیم، کار میکند.
بنابراین هرچند containerها به معنای نرم افزارهای از پیش نصب شدهی در حال اجرا هستند، اما ... به همراه ایزوله سازیهای قابل توجهی بر روی کامپیوتر میزبان اجرا میشوند؛ درست مانند یک کامپیوتر مجزای از آن.
در این مثال برای نمایش پیام به صورت notification، از کتابخانه toastr استفاده میکنیم که از طریق nuget میتوانید آن را به پروژه اضافه کنید:
PM> Install-Package toastr
toastr.info("نمایش یک پیام - info"); toastr.success("نمایش یک پیام - success"); toastr.error("نمایش یک پیام - error"); toastr.warning("نمایش یک پیام - warning");
toastr.success("نمایش یک پیام - success", "عنوان");
toastr.options = { tapToDismiss: true, toastClass: 'toast', containerId: 'toast-container', debug: false, showMethod: 'fadeIn', //fadeIn, slideDown, and show are built into jQuery showDuration: 300, showEasing: 'swing', //swing and linear are built into jQuery onShown: undefined, hideMethod: 'fadeOut', hideDuration: 1000, hideEasing: 'swing', onHidden: undefined, extendedTimeOut: 1000, iconClasses: { error: 'toast-error', info: 'toast-info', success: 'toast-success', warning: 'toast-warning' }, iconClass: 'toast-info', positionClass: 'toast-top-right', timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky titleClass: 'toast-title', messageClass: 'toast-message', target: 'body', closeHtml: '<button>×</button>', newestOnTop: true, preventDuplicates: false, progressBar: false };
public class NotificationHub : Hub { private readonly IProductService _productService; public NotificationHub(IProductService productService) { _productService = productService; } public void SendNotification() { Clients.Others.ShowNotification(_productService.GetLastProduct()); } }
var notify = $.connection.notificationHub; notify.client.showNotification = function (data) { toastr.info("رکورد جدیدی ثبت گردید جهت نمایش اینجا کلیک کنید"); }; $.connection.hub.start().done(function () { @{ if (ViewBag.NotifyUsers) { <text>notify.server.sendNotification();</text> } } });
var positionClasses = { topRight: 'toast-top-right', bottomRight: 'toast-bottom-right', bottomLeft: 'toast-bottom-left', topLeft: 'toast-top-left', topCenter: 'toast-top-center', bottomCenter: 'toast-bottom-center' }; var notify = $.connection.notificationHub; notify.client.showNotification = function (data) { toastr.options = { showDuration: 300, positionClass: positionClasses.bottomRight, onclick: function () { $('#table tr:last').after("<tr>" + "<td>" + data.Title + "</td>" + "<td>" + data.Description + "</td>" + "<td>" + data.Price + "</td>" + "<td>" + data.Category + "</td>" + "<td> </td>" + "</tr>"); } }; toastr.info("رکورد جدیدی ثبت گردید جهت نمایش اینجا کلیک کنید"); }; $.connection.hub.start().done(function () { @{ if (ViewBag.NotifyUsers) { <text>notify.server.sendNotification();</text> } } });
onclick: function () { $('#table tr:last').after("<tr>" + "<td>" + data.Title + "</td>" + "<td>" + data.Description + "</td>" + "<td>" + data.Price + "</td>" + "<td>" + data.Category + "</td>" + "<td> </td>" + "</tr>"); }
data {Id: 12, Title: "Item1", Description: "Des", Price: 100000, Category: 0}
<form asp-controller="Admin" asp-action="CreateBlog" enctype="multipart/form-data" data-ajax="true" data-ajax-method="post" data-ajax-loading="#Progress" data-ajax-complete="onComplete" data-ajax-failure="onFailed" data-ajax-success="onSuccess">
public async Task<IActionResult> CreateBlog(MyViewModel vm, IFormFile attachFile)
سوال: چگونه این فایل را در Jcenter آپلود کنیم؟
فرآیندی که در این نوشتار قصد داریم دنبال شود شامل مراحل زیر است:
ابتدا کتابخانهی خودمان را روی جی سنتر قرار داده و در صورتیکه علاقه داشته باشیم، آن را به mavenCentral هم انتقال میدهیم.
ابتدا نیاز است در سایت bintray ثبت نام کنید و با حساب جدید وارد شوید و گزینهی maven را انتخاب کنید.
سپس روی گزینهی Add New Package کلیک کنید تا یک پکیج جدید را ایجاد کنیم.
در صفحهای که باز میشود، اطلاعات مربوط به این پکیج را وارد کنید که عموما شامل نام پکیج، مجوز آن، کلمات کلیدی، لینک گزارش باگ و .. میشود. در انتخاب نام پکیج، قانون اجباری یا خاصی وجود ندارد؛ ولی توصیه میشود که از حروف کوچک و - استفاده گردد. بعد از پرکردن فیلدهای الزامی، وارد صفحهی جزئیات پکیج میشوید که در آن فیلدهای اضافهتری نیز وجود دارند که میتوانید در صورت تمایل آنها را پر کنید. همچنین در بالای صفحه لینک به صفحهی اختصاصی این پکیج نیز وجود دارد که در زیر عبارت Edit Package قرار گرفته است.
پی نوشت : اگر قصد آپلود کتابخانهی خود را در این سایت ندارید، میتوانید این سوال و مرحلهی امضای خودکار را از مراحل کاری خود حذف کنید.
سوال: چگونه این فایل را در SonaType آپلود کنیم؟
گام اول: ابتدا باید در سایت ثبت نام کنید. پس به این صفحه رفته و ثبت نام کنید. سپس در یک مرحلهی غیرمنطقی باید یک issue توسط سیستم JIRA ایجاد کنید. برای همین گزینهی Creare را در بالای صفحه بزنید. اطلاعات زیر را به ترتیب پر کنید:
Project: Community Support - Open Source Project Repository Hosting Issue Type: New Project Summary: مثلا نام پروژه خودتان را بنویسید یک نام پکیج که سعی کنید کتابخانههای هم خانواده این اشتراک را داشته باشند که در یک گروه قرار بگیرند Group Id: AndroidBreadCrumb.Plus آدرس جایی که پروژه قرار دارد Project URL: https://github.com/yeganehaym/AndroidBreadCrumb //آدرس سیستم کنترل نسخه SCM url: https://github.com/yeganehaym/AndroidBreadCrumb
فعال سازی امضای خودکار در Bintray
همانطور که در ابتدای مقاله گفتیم، میخواهیم کتابخانهی خود را از طریق jcenter به maven ارسال کنیم. برای همین نیاز داریم که ابتدا کتابخانهی خود را امضا کنیم. برای اینکار باید از طریق GPG یک کلید بسازیم. ساخت کلید به این شیوه، قبلا در مقالهی «ساخت کلیدهای امنیتی با GnuPG» توضیح داده شد و از تکرار آن خودداری میکنیم. تنها به ذکر این نکته بسنده میکنیم که شما باید یک کلید ساخته و آن را به سرور کلیدها ارسال کنید و سپس کلید متنی عمومی و خصوصی آن را در پروفایل bintray برگهی GPG Signing درج کنید.
این تنظیم از این پس بر روی تمامی کتابخانهها اعمال میشود.
سوال : چگونه پروژهی اندرویدی خودم را کامپایل کنم؟
فایل build.gradle پروژه را باز کنید و پلاگین bintray را به آن معرفی کنید:
dependencies { classpath 'com.android.tools.build:gradle:1.2.2' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' classpath 'com.github.dcendents:android-maven-plugin:1.2' }
bintray.user=YOUR_BINTRAY_USERNAME bintray.apikey=YOUR_BINTRAY_API_KEY bintray.gpg.password=YOUR_GPG_PASSWORD
در مرحلهی بعدی خطوط زیر را بعد از 'Apply Plugin 'com.android.library اضافه کنید و اطلاعاتی که در bintray وارد کردهاید را در اینجا وارد کنید:
apply plugin: 'com.android.library' ext { bintrayRepo = 'maven' bintrayName = 'AndroidBreadCrumb' publishedGroupId = 'com.plus' libraryName = 'AndroidBreadCrumb' artifact = 'AndroidBreadCrumb' libraryDescription = 'create breadcrumb on android to show a path to user and let user to jump on them' siteUrl = 'https://github.com/yeganehaym/AndroidBreadCrumb' gitUrl = 'https://github.com/yeganehaym/AndroidBreadCrumb' libraryVersion = '1.0' developerId = 'yeganehaym' developerName = 'ali yeganeh.m' developerEmail = 'yeganehaym@gmail.com' licenseName = 'The Apache Software License, Version 2.0' licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' allLicenses = ["Apache-2.0"] }
apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/installv1.gradle' apply from: 'https://raw.githubusercontent.com/nuuneoi/JCenter/master/bintrayv1.gradle'
compile 'com.plus:AndroidBreadCrumb:1.0'
آپلود فایلها به مخزن
برای آپلود فایلهای ماژول به مخزن، ابتدا ترمینال اندروید استودیو را باز کنید و گامهای زیر را به ترتیب انجام بدهید:
گام اول: با ارسال دستور زیر از صحت کدها و منابع مطمئن میشویم:
gradlew install
BUILD SUCCESSFUL
gradlew bintrayUpload
SUCCESSFUL
حال صفحهی اختصاصی پکیجتان را چک کنید. میبینید که قسمتهایی از آن تغییر کردهاست و قسمت نسخه، به روز شده است:
و قسمت فایلها هم دیگر خالی نیست:
با اینکه کتابخانهی ما روی maven قرار گرفت، ولی هنوز نمیتوان آن را توسط jcenter استفاده کرد و باید bintray maven را با jcenter هماهنگ نماییم. در حال حاضر استفاده از این کتابخانه بدون سینک به شکل زیر است:
گریدل پروژه maven{ url 'https://dl.bintray.com/yeganehaym/maven' } گریدل ماژول dependencies { compile 'com.plus:AndroidbreadCrumb:1.0' }
برای افزودن کتابخانهی خود به سیستم jcenter با کلیک بر روی گزینهی Add to jcenter میتوانید به تیم jcenter درخواست دهید که آن را تایید کنند که بعد از درخواست حدود سه ساعت طول میکشد تا پاسخ شما را بدهند.
به این ترتیب دیگر نیازی به تعریف یک url به maven نخواهد بود.
برای دیدن این کتابخانه در صفحه jcenter به ترتیب شناسههای Group_ID.Artifact.version را دنبال کنید، یعنی برای ما میشود:
com/plus/androidbreadcrumb/1.0
نکته دوم: در صورتی که پکیج خودتان را حذف کنید، چیزی از روی jcenter حذف نمیشود. فقط به یاد داشته باشید که برای حذف آن باید ابتدا نسخههای مختلف آپلود شده را حذف کنید تا پکیج از جی سنتر هم حذف شود.
در این مرحله قصد داریم که این کتابخانه را بر روی mavenCentral هم داشته باشیم. اگر قصدش را ندارید از اینجا به بعد را نیازی نیست انجام بدهید و برای اینکار لازم است همهی مراحل بالا انجام گرفته باشد.
قبل از اینکه این عمل ارسال انجام گیرد، باید دو عمل زیر از قبل صورت گرفته باشند:
- پکیج شما در jcenter تایید شده باشد.
- با مخزن شما در sonatype موافقت شده باشد.
در صورتیکه دو مرحلهی بالا صورت گرفته باشند، در صفحهی پکیج اختصاصی، بر روی گزینهی mavenCentral کلیک کنید:
پس از آن باید نام کاربری و کلمهی عبورتان را در SonaType، وارد کنید و گزینهی sync را بفشارید:
در صورتیکه پیام موفقیت در سینک را بدهد، پکیج شما منتقل شدهاست. در غیر این صورت خطای آن را اعلام میکند و باید برای رفع آن تلاش کنید تا خطاها از بین بروند. برای اینکه بتوانید این پکیج را در لیست mavenCentral ببینید، مثل همان چیزی که در بالاتر گفته شد، شناسهی گریدل را دنبال کنید.
ایجاد پروژههای خالی Blazor
در انتهای قسمت قبل، با روش ایجاد پروژههای خالی Blazor به کمک NET SDK 5x. آشنا شدیم. به همین جهت دو پوشهی جدید BlazorWasmSample و BlazorServerSample را ایجاد کرده و از طریق خط فرمان و با کمک NET CLI.، در پوشهی اولی دستور dotnet new blazorwasm و در پوشهی دومی دستور dotnet new blazorserver را اجرا میکنیم.
البته اجرای این دو دستور، نیاز به اتصال به اینترنت را هم برای بار اول دارند؛ تا فایلهای مورد نیاز و بستههای مرتبط را دریافت و restore کنند. بسته به سرعت اینترنت، حداقل یک ربعی را باید صبر کنید تا دریافت ابتدایی بستههای مرتبط به پایان برسد. برای دفعات بعدی، از کش محلی NuGet، برای restore بستههای blazor استفاده میشود که بسیار سریع است.
بررسی ساختار پروژهی Blazor Server و اجرای آن
پس از اجرای دستور dotnet new blazorserver در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همانطور که مشاهده میکنید، ساختار این پروژه، بسیار شبیه به یک پروژهی استاندارد ASP.NET Core از نوع Razor pages است.
- در پوشهی properties آن، فایل launchSettings.json قرار دارد که برای نمونه، شماره پورت اجرایی برنامه را در حالت اجرای توسط دستور dotnet run و یا توسط IIS Express مشخص میکند.
- پوشهی wwwroot آن، مخصوص ارائهی فایلهای ایستا مانند wwwroot\css\bootstrap است. در ابتدای کار، این پوشه به همراه فایلهای CSS بوت استرپ است. در ادامه اگر نیاز باشد، فایلهای جاوا اسکریپتی را نیز میتوان به این قسمت اضافه کرد.
- در پوشهی Data آن، سرویس تامین اطلاعاتی اتفاقی قرار دارد؛ به نام WeatherForecastService که هدف آن، تامین اطلاعات یک جدول نمایشی است که در ادامه در آدرس https://localhost:5001/fetchdata قابل مشاهده است.
- در پوشهی Pages، تمام کامپوننتهای Razor برنامه قرار میگیرند. یکی از مهمترین صفحات آن، فایل Pages\_Host.cshtml است. کار این صفحهی ریشه، افزودن تمام فایلهای CSS و JS، به برنامهاست. بنابراین در آینده نیز از همین صفحه برای افزودن فایلهای CSS و JS استفاده خواهیم کرد. اگر به قسمت body این صفحه دقت کنیم، تگ جدید کامپوننت قابل مشاهدهاست:
<body> <component type="typeof(App)" render-mode="ServerPrerendered" />
همچنین در همینجا، تگهای دیگری نیز قابل مشاهده هستند:
<body> <component type="typeof(App)" render-mode="ServerPrerendered" /> <div id="blazor-error-ui"> <environment include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </environment> <environment include="Development"> An unhandled exception has occurred. See browser dev tools for details. </environment> <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.server.js"></script> </body>
- در پوشهی Shared، یکسری فایلهای اشتراکی قرار دارند که قرار است در کامپوننتهای واقع در پوشهی Pages مورد استفاه قرار گیرند. برای نمونه فایل Shared\MainLayout.razor، شبیه به master page برنامههای web forms است که قالب کلی سایت را مشخص میکند. داخل آن Body@ را مشاهده میکنید که به معنای نمایش صفحات دیگر، دقیقا در همین محل است. همچنین در این پوشه فایل Shared\NavMenu.razor نیز قرار دارد که ارجاعی به آن در MainLayout.razor ذکر شده و کار آن نمایش منوی آبیرنگ سمت چپ صفحهاست.
- در پوشهی ریشهی برنامه، فایل Imports.razor_ قابل مشاهدهاست. مزیت تعریف usingها در اینجا این است که از تکرار آنها در کامپوننتهای razor ای که در ادامه تهیه خواهیم کرد، جلوگیری میکند. هر using تعریف شدهی در اینجا، در تمام کامپوننتها، قابل دسترسی است؛ به آن global imports هم گفته میشود.
- در پوشهی ریشهی برنامه، فایل App.razor نیز قابل مشاهدهاست. کار آن تعریف قالب پیشفرض برنامهاست که برای مثال به Shared\MainLayout.razor اشاره میکند. همچنین کامپوننت مسیریابی نیز در اینجا ذکر شدهاست:
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router>
- فایل appsettings.json نیز همانند برنامههای استاندارد ASP.NET Core در اینجا مشاهده میشود.
- فایل Program.cs آن که نقطهی آغازین برنامهاست و کار فراخوانی Startup.cs را انجام میدهد، دقیقا با یک فایل Program.cs برنامهی استاندارد ASP.NET Core یکی است.
- در فایل Startup.cs آن، همانند قبل دو متد تنظیم سرویسها و تنظیم میانافزارها قابل مشاهدهاست.
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddServerSideBlazor(); services.AddSingleton<WeatherForecastService>(); }
قسمتهای جدید متد Configure آن، ثبت مسیریابی توکار BlazorHub است که مرتبط است با اتصال دائم SignalR برنامه و اگر مسیری پیدا نشد، به Host_ هدایت میشود:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); }); }
که به همراه 13 درخواست و نزدیک به 600 KB دریافت اطلاعات از سمت سرور است.
این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. اگر صفحهی شمارشگر آنرا باز کنیم، با کلیک بر روی دکمهی آن، هرچند مقدار current count افزایش مییابد، اما شاهد post-back متداولی به سمت سرور نیستیم و این صفحه بسیار شبیه به صفحات برنامههای SPA (تک صفحهای وب) به نظر میرسد:
همانطور که عنوان شد، مدخل blazor.server.js فایل Pages\_Host.cshtml، کار به روز رسانی UI و هدایت رخدادها را به سمت سرور به صورت خودکار انجام میدهد. به همین جهت است که post-back ای را مشاهده نمیکنیم و برنامه، شبیه به یک برنامهی SPA به نظر میرسد؛ هر چند تمام رندرهای آن سمت سرور انجام میشوند و توسط SignalR به سمت کلاینت بازگشت داده خواهند شد.
برای نمونه اگر بر روی دکمهی شمارشگر کلیک کنیم، در برگهی network مرورگر، هیچ اثری از آن مشاهده نمیشود (هیچ رفت و برگشتی را مشاهده نمیکنیم). علت اینجا است که اطلاعات متناظر با این کلیک، از طریق web socket باز شدهی توسط SignalR، به سمت سرور ارسال شده و نتیجهی واکنش به این کلیکها و رندر HTML نهایی سمت سرور آن، از همین طریق به سمت کلاینت بازگشت داده میشود.
بررسی ساختار پروژهی Blazor WASM و اجرای آن
پس از اجرای دستور dotnet new blazorwasm در یک پوشهی خالی و ایجاد پروژهی ابتدایی آن:
همان صفحات پروژهی خالی Blazor Server در اینجا نیز قابل مشاهده هستند. این برنامهی نمونه، به همراه سه صفحهی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شدهاست. صفحات و کامپوننتهای پوشههای Pages و Shared نیز دقیقا همانند پروژهی Blazor Server قابل مشاهده هستند. مفاهیمی مانند فایلهای Imports.razor_ و App.razor نیز مانند قبل هستند.
البته در اینجا فایل Startup ای مشاهده نمیشود و تمام تنظیمات آغازین برنامه، داخل فایل Program.cs انجام خواهند شد:
namespace BlazorWasmSample { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync(); } } }
تفاوت ساختاری دیگر این پروژهی WASM، با نمونهی Blazor Server، ساختار پوشهی wwwroot آن است:
که به همراه فایل جدید نمونهی wwwroot\sample-data\weather.json است؛ بجای سرویس متناظر سمت سرور آن در برنامهی blazor server. همچنین فایل جدید wwwroot\index.html نیز قابل مشاهدهاست و محتوای تگ body آن به صورت زیر است:
<body> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.webassembly.js"></script> </body>
- در ابتدای بارگذاری برنامه، یک loading نمایش داده میشود که در اینجا نحوهی تعریف آن مشخص است. همچنین اگر خطایی رخ دهد نیز توسط div ای با id مساوی blazor-error-ui اطلاع رسانی میشود.
- مدخل blazor.webassembly.js در اینجا، کار دریافت وب اسمبلی و فایلهای NET runtime. را انجام میدهد؛ برخلاف برنامههای Blazor Server که توسط فایل blazor.server.js، یک ارتباط دائم SignalR را با سرور برقرار میکردند تا کدهای رندر شدهی سمت سرور را دریافت و نمایش دهند و یا اطلاعاتی را به سمت سرور ارسال کنند: برای مثال بر روی دکمهای کلیک شدهاست، اطلاعات مربوطه را در سمت سرور پردازش کن و نتیجهی نهایی رندر شده را بازگشت بده. اما در اینجا همه چیز داخل مرورگر اجرا میشود و برای این نوع اعمال، رفت و برگشتی به سمت سرور صورت نمیگیرد. به همین جهت تمام کدهای #C ما به سمت کلاینت ارسال شده و داخل مرورگر به کمک فناوری وب اسمبلی، اجرا میشوند. در اینجا از لحاظ ارسال تمام کدهای مرتبط با UI برنامهی سمت کلاینت به مرورگر کاربر، تفاوتی با فریمورکهایی مانند Angular و یا React نیست و آنها هم تمام کدهای UI برنامه را کامپایل کرده و یکجا ارسال میکنند.
در ادامه در همان پوشه، دستور dotnet run را اجرا میکنیم تا پروژه کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.
که به همراه 205 درخواست و نزدیک به 9.6 MB دریافت اطلاعات از سمت سرور است. البته اگر همین صفحه را refresh کنیم، دیگر شاهد دریافت مجدد فایلهای DLL مربوط به NET Runtime. نخواهیم بود و اینبار از کش مرورگر خوانده میشوند:
در این برنامهی سمت کلاینت، ابتدا تمام فایلهای NET Runtime. و وب اسمبلی دریافت شده و سپس اجرای تغییرات UI، در همین سمت و بدون نیاز به اتصال دائم SignalR ای به سمت سرور، پردازش و نمایش داده میشوند. به همین جهت زمانیکه بر روی دکمهی شمارشگر آن کلیک میکنیم، اتفاقی در برگهی network مرورگر ثبت نمیشود و رفت و برگشتی به سمت سرور صورت نمیگیرد.
عدم وجود اتصال SignalR، مزیت امکان اجرای آفلاین برنامهی WASM را نیز میسر میکند. برای مثال یکبار دیگر همان برنامهی Blazor Server را به کمک دستور dotnet run اجرا کنید. سپس آنرا در مرورگر در آدرس https://localhost:5001 باز کنید. اکنون پنجرهی کنسولی که dotnet run را اجرا کرده، خاتمه دهید (قسمت اجرای سمت سرور آنرا ببندید).
بلافاصله تصویر «سعی در اتصال مجدد» فوق را مشاهده خواهیم کرد که به دلیل قطع اتصال SignalR رخ دادهاست. یعنی یک برنامهی Blazor Server، بدون این اتصال دائم، قادر به ادامهی فعالیت نیست. اما چنین محدودیتی با برنامههای Blazor WASM وجود ندارد.
البته بدیهی است اگر یک Web API سمت سرور برای ارائهی اطلاعاتی به برنامهی WASM نیاز باشد، API سمت سرور برنامه نیز باید جهت کار و نمایش اطلاعات در سمت کلاینت در دسترس باشد؛ وگرنه قسمتهای متناظر با آن، قادر به نمایش و یا ثبت اطلاعات نخواهند بود.
واژهی استثناء یا exception کوتاه شدهی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ میدهد و در نتیجه، جریان عادی برنامه را مختل میکند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی دربارهی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته میشود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر میکند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامهی شما باشد و باید این لیست را درون برنامهی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامهی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامهتان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی میتوانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناءها بستگی به حالتهای مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه میتواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts() { using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml"))) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
در مثال دوم میدانیم که ممکن است فایل از قبل موجود نباشد. بنابراین میتوانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts() { var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml"); if (!File.Exists(fullPath)) return new Product[0]; using (var stream = File.Read(fullPath)) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر میگردانند ایجاد کرد.
اجازه دهید دوباره از مثالهای فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
IEnumerable<Product> GetCachedProducts()
در واقع استثناها حالتهایی هستند که غیرقابل پیشبینی هستند. این حالتها میتوانند یک خطای منطقی از طرف برنامهنویس و یا چیزی خارج کنترل برنامهنویس باشند (مانند خطاهای سیستمعامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمیتوان مدیریت کرد.
اگر میخواهید استثناءها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.
چه استثناءهایی باید مدیریت شوند و کدامها خیر؟
مدیریت صحیح استثناءها میتواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ میدهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری میکند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناءهایی را catch کنید که بتوانید برای آن راهحلی ارائه دهید.
به عنوان مثال اگر در لایهی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، میتوانیم آن را catch کرده و درون یک استثناء عمومیتر قرار دهیم:
public class UserRepository : IUserRepository { public IList<User> Search(string value) { try { return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value)); } catch (SqlException err) { var msg = String.Format("Ohh no! Failed to search after users with '{0}' as search string", value); throw new DataSourceException(msg, err); } } }
اگر مطمئن نیستید که تمام استثناءها توسط شما مدیریت شدهاند، میتوانید در حالتهای زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: میتوانید Aplication_Error را پیادهسازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException
WCF: پیادهسازی اینترفیس IErrorHandler
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI
چه زمانهایی باید یک استثناء صادر شود؟
صادر کردن یک استثناء به تنهایی کار سادهایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه دادههایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.
اعتبارسنجی آرگومانها
سادهترین مثال، آرگومانهای مورد انتظار یک متد است:
public void PrintName(string name) { Console.WriteLine(name); }
مشکل فوق را میتوانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); Console.WriteLine(name); }
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long"); if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics"); Console.WriteLine(name); }
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحثبرانگیز متدی با امضای زیر است:
public User GetUser(int id) { }
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId); if (user == null) throw new InvalidOperationException("Failed to find user: " + userId); // actual logic here
public User GetUser(int id) { if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?"); var user = db.Execute<User>("WHERE Id = ?", id); if (user == null) throw new EntityNotFoundException("Failed to find user with id " + id); return user; }
خطاهای متداول حین کار با استثناءها
- صدور مجدد استثناء و از بین بردن stacktrace
کد زیر را در نظر بگیرید:
try { FutileAttemptToResist(); } catch (BorgException err) { _myDearLog.Error("I'm in da cube! Ohh no!", err); throw err; }
- اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید
یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:
try { GreaseTinMan(); } catch (InvalidOperationException err) { throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود }
- ارائه ندادن context information
در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:
try { socket.Connect("somethingawful.com", 80); } catch (SocketException err) { throw new InvalidOperationException("Socket failed", err); }
void IncreaseStatusForUser(int userId, int newStatus) { try { var user = _repository.Get(userId); if (user == null) throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus)); user.Status = newStatus; _repository.Save(user); } catch (DataSourceException err) { var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus); throw new UpdateException(errMsg, err); }
نحوهی طراحی استثناءها
برای ایجاد یک استثناء سفارشی میتوانید از کلاس Exception ارثبری کنید و چهار سازندهی آن را اضافه کنید:
public NewException() public NewException(string description ) public NewException(string description, Exception inner) protected or private NewException(SerializationInfo info, StreamingContext context)
سازندهی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کردهاید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
“The key ‘abrakadabra’ was not present in the dictionary.”
سازندهی سوم شبیه به سازندهی قبلی عمل میکند با این تفاوت که توسط پارامتر دوم میتوانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازندهی سوم زمانی مورد استفاده قرار میگیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیرهی استثناءها درون فایل و...)
خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception { public SampleException(string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); } public SampleException(string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); } public SampleException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
اجباری کردن ارائهی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازندهها را اینگونه تعریف کنیم:
public class HttpException : Exception { System.Net.HttpStatusCode _statusCode; public HttpException(System.Net.HttpStatusCode statusCode, string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); _statusCode = statusCode; } public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); _statusCode = statusCode; } public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { } public System.Net.HttpStatusCode StatusCode { get; private set; } }
public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } }
public class HttpException : Exception { // [...] public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { // this is new StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode"); } public HttpStatusCode StatusCode { get; private set; } public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } } // this is new public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("HttpStatusCode", (int) StatusCode); } }
در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را میتوان ارائه داد، این مورد در یافتن راهحل خیلی کمک خواهد کرد.
طراحی پیامهای مناسب
پیامهای exception مختص به توسعهدهندگان است نه کاربران نهایی.
نوشتن این نوع پیامها برای برنامهنویس کار خستهکنندهایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType"); throw new Exception("Unecpected workingDirectory");
توسعهدهندگانی که exception message را در اولویت قرار میدهند، معتقد هستند که از لحاظ تجربهی کاربری پیامها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیشتر عنوان گردید این نوع پیامها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایههای زیرین. همچنین پیامهایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامهنویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیامها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیامها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوهی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیامهای استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیامها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناءهایی که توسط Base Class Library و دیگر کتابخانههای thirt-party صادر میشوند چیست؟ اینها انگلیسی هستند.
در تمامی حالتهایی که عنوان شد فرض بر این است که شما در حال نوشتن این نوع پیامها برای یک سیستم خاص هستید. اما اگر هدف نوشتن یک کتابخانه باشد چطور؟ در این حالت نمیدانید که کتابخانهی شما در کجا استفاده میشود.
اگر هدف نوشتن یک کتابخانه نباشد این نوع پیامهایی که برای کاربران نهایی باشند، وابستگیها را در سیستم افزایش خواهند داد، زیرا در این حالت پیامها به یک رابط کاربری خاص گره خواهند خورد.
خب اگر پیامها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام میتواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیامهای زیر مناسب هستند:
"Unecpected workingDirectory"
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory. "The invalid value was: "fllobdedy"."
همیشه برای نوشتن پیامهای مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوهی رفع مشکل را توضیح دهید
پراپرتی سفارشی در EF Database First
listOfActualTags از بانک اطلاعاتی دریافت شده؛ بر اساس مواردی که موجود بوده.
به این ترتیب چون این تگها به سیستم ردیابی EF وارد میشوند و همچنین post1.Tags.Clear در ابتدای کار فراخوانی شده، استفاده از متد (post1.Tags.Add(item سبب ثبت مورد تکراری نخواهد شد.
کلا EF هر آیتمی رو که Id آنرا از طریق دریافت اطلاعات از بانک اطلاعاتی در سیستم ردیابی خودش داشته باشه، جدید و تکراری ثبت نمیکنه. برای نمونه در حالت new Tag استفاده شده، این موارد جدید ثبت میشوند چون Id از قبل ثبت شدهای ندارند.
برای توضیحات بیشتر مراجعه کنید به مطلب نحوه استفاده از کلیدهای خارجی در EF. (حتی میشود یک شیء را بدون واکشی از دیتابیس به سیستم ردیابی وارد کرد؛ البته اگر Id آنرا داشته باشید)