مطالب
آشنایی با ذخیره سازی در حافظه
آشنایی با Virtual Address spaces
فضای آدرس‌دهی مجازی: موقعی که یک پردازشگر در مکانی از حافظه عمل خواندن و نوشتن را آغاز می‌کند، از آدرس‌های مجازی بهره می‌برد. بخشی از عملیات خواندن و نوشتن، تبدیل آدرس‌های مجازی به آدرس‌های فیزیکی در حافظه است. این عمل سه مزیت دارد:

  1. آدرس‌های مجازی به صورت پیوسته و پشت سر هم هستند و آدرس دهی بسیار راحت است ولی داده‌ها بر روی یک حافظه به صورت متصل به هم یا پیوسته ذخیره یا خوانده نمی‌شوند و کار آدرس دهی مشکل است. پس یکی از مزایای داشتن آدرس دهی مجازی پشت سر هم قرار گرفتن آدرس هاست.
  2. برنامه از آدرس‌های مجازی برای دسترسی به بافر حافظه استفاده می‌کند که بزرگتر از حافظه فیزیکی موجود هست. موقعی که نیاز به حافظه بیشتر باشد و حافظه سیستم کوچکتر یا کمتر از تقاضا باشد، مدیر حافظه، صفحات حافظه فیزیکی را به صورت یک فایل (عموما 4 کیلیویی) بر روی دیسک سخت ذخیره می‌کند و صفحات داده‌ها در موقع نیاز بین حافظه فیزیکی و دیسک سخت جابجا می‌شود.
  3. هر پردازشی که بر روی آدرس‌های مجازی کار می‌کند ایزوله شده است. یعنی یک پروسه هیچ گاه نمیتواند به آدرس‌های یک پروسه دیگر دسترسی داشته باشد و باعث تخریب داده‌های آن شود.
به محدوده شروع آدرس‌های مجازی تا پایان آن محدوده، فضای آدرس‌دهی مجازی گویند. هر پروسه ای که در مد کاربر آغاز میشود از یک فضای آدرس خصوصی یا مختص به خود استفاده می‌کند. برای سیستم‌های 32 بیتی این فضا میتواند دو گیگ باشد که از آدرس   0x00000000 شروع می‌شود و تا 0x7FFFFFFF  ادامه پیدا می‌کند و برای یک سیستم 64 بیتی تا 8 ترابایت می‌باشد که از آدرس  0x000'00000000  تا آدرس  0x7FF'FFFFFFFF ادامه می‌یاید. گاهی اوقات به محدوده آدرس‌های مجازی، حافظه مجازی می‌گویند.
شکل زیر اصلی‌ترین خصوصیات فضای آدرس‌های مجازی را نشان می‌دهد:

در شکل بالا دو پروسه 64 بیتی به نام‌های notepad.exe و myapp.exe قرار دارند که هر کدام فضای آدرس‌های مجازی خودشان را دارند و از آدرس  0x000'0000000 شروع و تا آدرس 0x7FF'FFFFFFFF ادامه میابند. هر قسمت شامل یک صفحه 4 کیلویی از حافظه مجازی یا فیزیکی است. به برنامه نوت‌پد دقت کنید که از سه صفحه پشت سر هم یا پیوسته تشکیل شده که آدرس شروع آن 0x7F7'93950000 می باشد ولی در حافظه فیزیکی خبری از پیوسته بودن دیده نمی‌شود و حتما این نکته را متوجه شدید که هر دو پروسه از یک آدرس شروع استفاده کرده‌اند، ولی به آدرسی متفاوت از حافظه فیزیکی نگاشت شده اند.

تفاوت user mode و kernel mode
هر پردازش در سیستم بر اساس user mode مد کاربر یا kernel mode مد کرنل اجرا میشود. پردازش‌ها بر اساس هر نوع کد بین این دو بخش سوییچ می‌کنند. اپلیکیشن‌ها بر اساس مد کاربر و هسته سیستم عامل و اکثر درایورها بر اساس مد کرنل کار می‌کنند؛ ولی تعدادی از آن‌ها هم در مد کاربر.
هر برنامه یا اپلیکیشنی که اجرا می‌شود، در یک مد کاربری قرار می‌گیرد. ویندوز هم برای هر برنامه یک پروسه یا فرآیندی را ایجاد می‌کند. پروسه برای برنامه یک فضای آدرس‌دهی مجازی و یک جدول مدیریت به صورت خصوصی یا مختص همین برنامه تشکیل می‌دهد. به این ترتیب هیچ برنامه دیگری نمی‌تواند به داده‌های برنامه دیگر دسترسی داشته باشد و  هر برنامه در یک محیط ایزوله شده برای خودش قرار میگیرد و این برنامه اگر به هر ترتیبی کرش کند، برنامه‌های دیگر به کار خود ادامه می‌دهند و هیچ تاثیری بر برنامه‌های دیگر نمی‌گذارند.
البته استفاده از این آدرس‌های مجازی محدودیت هایی هم دارد، چرا که بعضی از آن‌ها توسط سیستم عامل رزرو شده اند و برنامه نمی‌تواند به آن قسمت‌ها دسترسی داشته باشد و این باعث می‌شود که داده‌های برنامه از خسارت و آسیب دیدن حفظ شوند.
تمام برنامه هایی در حالت کرنل ایجاد می‌شوند، از یک فضای آدرس مجازی استفاده می‌کنند. به این معنی که یک درایور مد کرنل نسبت به دیگر درایورها و خود سیستم عامل به هیچ عنوان در یک محیط ایزوله قرار ندارد. بنابراین ممکن است یک کرنل درایور تصادفا در یک آدرس مجازی اشتباه که میتواند متعلق به سیستم عامل یا یک درایور دیگر باشد بنویسد. یعنی اگر یک درایور کرنل کراش کند کل سیستم عامل کرش میکند.
تصویر زیر به خوبی ارتباط بین مد کاربری و مد کرنل را نشان می‌دهد:

 

فضای کاربری و فضای سیستمی User space and system space

گفتیم بسیاری از پروسه‌ها در حالت user mode و پروسه‌های هسته سیستم عامل و درایورها در حالت kernel mode اجرا می‌شوند. هر پروسه مد کاربر از فضای آدرس دهی مجازی خودش استفاده می‌کند ولی در حالت کرنل همه از یک فضای آدرس دهی استفاده می‌کنند که به آن فضای سیستمی می‌گویند و برای مد کاربری می‌گویند فضای کاربری.

در سیستم‌های 32 بیتی نهایتا تا 4 گیگ حافظه می‌توان به این‌ها تخصیص داد؛ 2 گیگ ابتدایی به user space و دو گیگ بعدی به system space :

در ویندوزهای 32 بیتی شما امکان تغییر این مقدار حافظه را در میان بوت دارید و می‌توانید حافظه کاربری را تا 3 گیگ مشخص کنید و یک گیگ را برای فضای سیستمی. برای اینکار می‌توانید از برنامه bcedit استفاده کنید.

در سیستم‌های 64 بیتی میزان حافظه‌های مجازی به صورت تئوری تا 16 اگزابایت مشخص شده است؛ ولی در عمل تنها بخش کوچکی از آن یعنی 8 ترابایت استفاده می‌شود.

 کدهایی که در user mode اجرا می‌شوند فقط به فضای کاربری دسترسی دارند و دسترسی آن‌ها به فضای سیستمی به منظور جلوگیری از تخریب داده ممکن نیست. ولی در حالت کرنل می‌توان به دو فضای سیستمی و کاربری دسترسی داشت. درایورهایی که در مدکرنل نوشته شده اند باید تمام دقت خود را در زمینه نوشتن و خواندن از فضای سیستمی در حافظه به کار گیرند. سناریوی زیر به شما نشان می‌دهد که چرا باید مراقب بود:
  1. برنامه جهت اجرا در مد کاربر یک درخواست را برای خواندن داده‌های یک device را آماده می‌کند. سپس برنامه آدرس شروع یک بافر را برای دریافت داده، مشخص می‌کند.
  2. وظیفه این درایور یک قطعه در مد کرنل این است که  عملیات خواندن را شروع کرده و کنترل را به درخواست کننده ارسال می‌کند.
  3. بعد device یک وقفه را به هر تردی thread که در حال اجراست ارسال می‌کند تا بگوید، عملیات خواندن پایان یافته است. این وقفه توسط ترد درایور مربوطه دریافت می‌شود.
  4. حالا دیگر درایور نباید داده‌ها را در همان جایی که گام اول برنامه مشخص کرده است ذخیره کند. چون این آدرس که برنامه در مد کاربری مشخص کرده است، با نمونه‌ای که این فرآیند محاسبه می‌کند متفاوت است.
Paged Pool and NonPaged Pool
در فضای کاربری تمام صفحات در صورت نیاز توانایی انتقال به دیسک سخت را دارند ولی در فضای سیستمی همه بدین صورت نیستند. فضای سیستمی دو ناحیه حافظه تخصیصی پویا دارد که به نام‌های paged pool و nonpaged pool شناخته می‌شوند.
در سیستم‌های 32 بیتی Pagedpool توانایی 128 گیگ فضای آدرس دهی مجازی را از آدرس 0xFFFFAC00'00000000 تا آدرس 0xFFFFAC1F'FFFFFFFF و در سیستم‌های 64 بیتی توانایی 128 گیگ فضای آدرس دهی مجازی را از 0xFFFFA800'00000000 تا 0xFFFFA81F'FFFFFFFF دارد. حافظه ای که به صورت paged pool تخصیص شده باشد می‌تواند صفحات حافظه را بر روی دیسک سخت ذخیره کند؛ ولی حافظه ای که به صورت nonpaged تخصیص یافته باشد، هرگز نمی‌تواند.

مطالب
راه اندازی Docker swarm
مقاله‌های زیادی درباره‌ی مزایای استفاده‌ی از داکر در اینترنت وجود دارند. در این مقاله قصد دارم طریقه‌ی راه اندازی یک سرور Production را برای داکر، توضیح دهم.
یکی از مزایای مهم داکر، امکان Scale در سریعترین زمان ممکن هست. یعنی اگر در محیط Production میزان بار بر روی یکی از اجزای محصول شما بیشتر بود (در صورتیکه معماری صحیحی برای سرویس‌های مجزا رعایت شده باشد)، می‌توانید آن قسمت را Scale کنید. می‌دانید که وجود بیشتر از یک Instance از یک سرویس، نگرانی‌های معمولی مثل Load Balancing  و غیره را به همراه دارد و اینکه کانتینر داکر شما قرار هست بر روی چه سروری نصب شود و بر روی چه سرور یا سرور‌هایی Scale شود.
ابزار‌های خوبی برای مدیریت کردن سرورهایی که Container‌های داکر بر روی آنها اجرا می‌شوند وجود دارند که یکی از مهمترین آنها kubernetes است که یکی از بهترین ابزارها، برای Management، Scaling و Deploy اتوماتیک نرم افزارهای Containerized می‌باشد.
در این مقاله از ابزاری به نام Swarm استفاده می‌کنم که راه اندازی آن راحت‌تر هست و همینطور وقتی احتیاج به Scaling وجود داشت، فقط یک PublishedPort به سرویس اختصاص داده می‌شود و  Load Balancing کاملا اتوماتیک انجام خواهد شد.
برای نصب Swarm، من دو سرور لینوکسی را آماده می‌کنم. یکی از سرور‌ها نقش Master را ایفا می‌کند و دیگری نقش Worker را (به راحتی می‌شود تعداد Worker‌ها را افزایش داد). 
از مزایای Swarm هم می‌شود به این نکته اشاره کرد که وقتی به سخت افزار بیشتری احتیاج باشد، کافی است یک ماشین دیگر را راه اندازی و آن را به عنوان یک Worker به Master معرفی کنیم و لازم نیست نگرانی درباره‌ی اینکه چه کانتینری بر روی چه سروری اجرا می‌شود داشته باشیم.
مشخصات سرور‌ها:
DocerMaster :
OS : CentOS7
IP: 192.168.64.3

DockerWorker:
OS: CentOS7
IP: 192.168.64.4
مطمئن شوید که هر دو در یک شبکه هستند و همدیگر را پینگ می‌کنند.
سپس بر روی هر دو سرور، داکر را نصب می‌کنم:
sudo yum install docker
و در ادامه سرویس داکر را  بر روی هر دو سیستم استارت می‌کنیم: 
$ sudo service docker start
$ sudo systemctl start docker.service
 مطمئن شوید که داکر در هر دو سرور نصب و اجرا شده‌است. با دستور service status docker می‌توانید نتیجه را ببینید.
بعد از نصب داکر، به صورت پیشفرض Swarm هم نصب می‌شود و لازم به نصب ابزار دیگری نیست.
برای ارتباط بین Master و Worker‌ها باید بعضی از پورت‌ها در این سرور‌ها باز شود. برای این کار در کامپیوتر Master دستورات زیر را اجرا کنید تا پورت‌ها به فایروال اضافه شوند: 
firewall-cmd --permanent --add-port=2376/tcp
firewall-cmd --permanent --add-port=2377/tcp
firewall-cmd --permanent --add-port=7946/tcp
firewall-cmd --permanent --add-port=7946/udp
firewall-cmd --permanent --add-port=4789/udp
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --reload
و سپس دستور زیر را اجرا کنید: 
systemctl restart docker
به کامپیوتر Worker رفته و با دستورات زیر پورت‌های لازم را به فایروال اضافه کنید: 
~]# firewall-cmd --permanent --add-port=2376/tcp
~]# firewall-cmd --permanent  --add-port=7946/tcp
~]# firewall-cmd --permanent --add-port=7946/udp
~]#  firewall-cmd --permanent --add-port=4789/udp
~]# firewall-cmd --permanent --add-port=80/tcp
~]#  firewall-cmd --reload
~]#  systemctl restart docker
در سرور Master باید Swarm را راه اندازی کنیم. برای این کار از دستور زیر استفاده می‌کنیم: 
sudo docker swarm init –advertise-addr 192.168.64.3
 بعد از اجرای موفقیت آمیز دستور فوق، عبارت زیر نمایش داده می‌شود: 

همانطور که مشاهده می‌کنید، پس از راه اندازی، اعلانی مبنی بر اینکه این نود به عنوان Manager شناخته شده و اینکه برای اضافه کردن یک نود Worker چه دستوری را باید اجرا کرد، نمایش داده شده‌است.

اکنون کافی‌است این خط کد را در نود Worker کپی کنیم: 

بعد از موفقیت آمیز بودن اجرای آن، می‌توانید در کامپیوتر Master، با دستور زیر تمام نود‌ها را مشاهده کنید: 

$ sudo docker node ls

همانطور که مشاهده می‌کنید، دو نود وجود دارد که یکی به عنوان Leader شناخته می‌شود. هر زمانی که نیاز بود، می‌شود به راحتی یک Worker دیگر را اضافه کرد.

برای راه اندازی یک کانتینر، swarm از CLI کاملی برخوردار هست؛ اما مایلم اینجا از یک ابزار خوب، برای مدیریت Swarm استفاده کنم. Portainer به عنوان یه ابزار عالی برای مدیریت Image‌ها و Container‌های داکر محسوب می‌شود که کاملا swarm را پشتیبانی می‌کند.

برای راه اندازی portainer کافی است کد زیر را در سیستم Master اجرا کنید: 

$ docker volume create portainer_data
$ docker run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer

البته به دلیل عدم دسترسی به داکر هاب از کشور ایران، عملا امکان pull کردن این image، مستقیما از داکر هاب و بدون وی پی ان وجود ندارد. 

بعد از موفقیت آمیز بودن راه اندازی portainer می‌توانید از طریق آدرس http://192.168.64.3:9000 به آن دسترسی داشته باشید. در اولین ورود، پسورد ادمین را تنظیم می‌کنید و بعد از وارد شدن، صفحه‌ای مطابق شکل زیر را خواهید دید:  

اگر بر روی منوی swarm کلیک کنید، همه‌ی نود‌ها را مشاهده خواهید کرد و در صورتیکه بر روی Containers کلیک کنید، همه‌ی Container هایی را که بر روی این سرور وجود دارند، خواهید دید. مهمترین قسمت، بخش Service هاست که مشخصات Container هایی که روی swarm توزیع شدن را نشان می‌دهد و همینطور تعداد Container هایی از این image که Scale شدند. همینطور که می‌بینید فعلا فقط همین Portainer در حال اجراست. 


اجازه دهید یک مثال کاربردی‌تر بزنیم و یک سرویس را ایجاد کنیم.

من بر روی کامپیوتر شخصی‌ام و نه سرورها، با دستور زیر یک پروژه‌ی MVC را با دات نت Core ایجاد می‌کنم:

dotnet new mvc

و سپس دستور dotnet publish را اجرا می‌کنم و به پوشه‌ای که محتویات پابلیش شده در آن قرار دارند رفته و یک فایل بدون پسوند را به نام dockerfile ایجاد می‌کنم و متن زیر را در آن می‌نویسم:

همینطور که می‌بینید من از image مخصوص اجرای دات نت Core در این container استفاده می‌کنم. پوشه‌ی کانتینر را تنظیم می‌کنم و همه‌ی فایل‌هایی که در پوشه‌ی جاری سیستم خودم وجود دارند را به پوشه‌ی جاری کانتینر منتقل می‌کنم و سپس دستور دات نت را با پارامتر اسم dll پروژه‌ام اجرا می‌کنم. این کل محتویات فایل داکر من هست.

ترمینال را در همین پوشه‌ی publish باز می‌کنم و دستور زیر را اجرا می‌کنم:

docker build –t swarmtest:dev .
با این دستور یک image از روی داکر فایلی که ایجاد کردیم درست می‌شود:

حالا باید این image را به سرور master منتقل کنیم. برای این کار راه‌های مختلفی وجود دارند که معمول‌ترین آن‌ها push کردن این image به یک registery و سپس pull کردن آن در کامپیوتر Master است که من از این راه استفاده نمی‌کنم. من این image را بر روی کامپیوترم ذخیره می‌کنم و سپس فایل ذخیره شده را به سرور master منتقل می‌کنم و آنجا آن فایل را load می‌کنم.
در ادامه بر روی کامپیوتر خودم دستور زیر را اجرا می‌کنم:
docker save swarmtest:dev –o swarmtest.tar

طبق شکل زیر یک فایل tar که حاوی image برنامه من هست، ایجاد شد:

حالا با دستور زیر این فایل رو به سرور Master منتقل می‌کنم:

scp –r swarmtest.tar root@192.168.64.3:/srv/images

همانطور که می‌بینید، فایل tar به پوشه‌ای که قبلا در سرور ایجاد کردم، منتقل شد.

حالا به سرور و پوشه‌ای که فایل tar آنجا قرار دارد رفته و با دستور زیر این image را بر روی سیستم load می‌کنم:

sudo docker load –i swarmtest.tar

همانطور که در تصویر می‌بینید، بعد از load شدن، image مورد نظرمان به داکر اضافه شده‌است.

حالا برای اجرا کردن این سرویس بر روی swarm، آدرس portainer را باز می‌کنیم و به قسمت  services می‌رویم و بر روی دکمه‌ی add service کلیک می‌کنیم:

در قسمت نام، نام سرویس و در قسمت imageConfiguration از منوی image‌ها، ایمیجی را که ایجاد کردیم، انتخاب می‌کنیم. در قسمت Replicas تعداد instance‌های container ای را که می‌خواهیم از روی image ایجاد شوند، مشخص می‌کنیم. (این قسمت را بر روی هر وضعیتی می‌توانیم قرار دهیم و زیاد و کم کنیم) و در قسمت port mapping، پورت درون Container و پورتی را که می‌خواهیم بر روی هاست به نمایش درآید، وارد می‌کنیم.

همانطور که می‌بینید من به راحتی می‌توانم تعداد Container‌ها را Scale کنم و نگرانی‌ای بابت load balancing و اینکه کدام container بر روی کدوم سرور ایجاد می‌شود، نخواهم داشت.

برای نمایش برنامه کافی است پورتی را که برای هاست وارد کردیم، با آی پی Master وارد کنیم:


مطالب
Blazor 5x - قسمت دوم - بررسی ساختار اولیه‌ی پروژه‌های Blazor
پس از آشنایی با دو مدل هاستینگ برنامه‌های Blazor در قسمت قبل، اکنون می‌خواهیم ساختار ابتدایی قالب‌های این دو پروژه را بررسی کنیم.


ایجاد پروژه‌های خالی 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" />
کار آن، رندر کامپوننت App.razor واقع در ریشه‌ی پروژه‌است.
همچنین در همینجا، تگ‌های دیگری نیز قابل مشاهده هستند:
<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>
همانطور که در قسمت قبل نیز عنوان شد، اتصال برنامه‌ی در حال اجرای در مرورگر blazor server با backend، از طریق یک اتصال دائم SignalR صورت می‌گیرد. اگر در این بین خطای اتصالی رخ دهد، div با id مساوی blazor-error-ui فوق، یکی از پیام‌‌هایی را که مشاهده می‌کنید، بسته به اینکه برنامه در حالت توسعه در حال اجرا است و یا در حالت ارائه‌ی نهایی، نمایش می‌دهد. افزودن مدخل blazor.server.js نیز به همین منظور است. کار آن مدیریت اتصال دائم SignalR، به صورت خودکار است و از این لحاظ نیازی به کدنویسی خاصی از سمت ما ندارد. این اتصال، کار به روز رسانی UI و هدایت رخ‌دادها را به سمت سرور، انجام می‌دهد.

- در پوشه‌ی 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>
اگر شخصی مسیر از پیش تعریف شده‌ای را وارد کند، به قسمت Found وارد می‌شود و در غیر اینصورت به قسمت NotFound. در قسمت Found است که از قالب MainLayout برای رندر یک کامپوننت توسط کامپوننت RouteView، استفاده خواهد شد و در قسمت NotFound، فقط پیام «Sorry, there's nothing at this address» به کاربر نمایش داده می‌شود. قالب‌های هر دو نیز قابل تغییر و سفارشی سازی هستند.

- فایل 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>();
}
در ConfigureServices آن، سرویس‌های صفحات razor و ServerSideBlazor اضافه شده‌اند. همچنین سرویس نمونه‌ی WeatherForecastService نیز در اینجا ثبت شده‌است.
قسمت‌های جدید متد Configure آن، ثبت مسیریابی توکار BlazorHub است که مرتبط است با اتصال دائم SignalR برنامه و اگر مسیری پیدا نشد، به Host_ هدایت می‌شود:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...

    app.UseEndpoints(endpoints =>
    {
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToPage("/_Host");
    });
}
در ادامه در همان پوشه، دستور dotnet run را اجرا می‌کنیم تا پروژه، کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.


که به همراه 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();
        }
    }
}
در اینجا ابتدا کامپوننت App.razor را به برنامه معرفی می‌کند که ساختار آن با نمونه‌ی مشابه Blazor Server دقیقا یکی است. سپس سرویسی را برای دسترسی به HttpClient، به سیستم تزریق وابستگی‌های برنامه معرفی می‌کند. هدف از آن، دسترسی ساده‌تر به endpoint‌های یک ASP.NET Core Web API است. از این جهت که در یک برنامه‌ی سمت کلاینت، دیگر دسترسی مستقیمی به سرویس‌های سمت سرور را نداریم و برای کار با آن‌ها همانند سایر برنامه‌های SPA که از Ajax استفاده می‌کنند، در اینجا از HttpClient برای کار با Web API‌های مختلف استفاده می‌شود.

تفاوت ساختاری دیگر این پروژه‌ی 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>
- چون این برنامه، یک برنامه‌ی سمت کلاینت است، اینبار بجای فایل Host_ سمت سرور، فایل index.html سمت کلاینت را برای ارائه‌ی آغازین برنامه داریم که وابستگی به دات نت ندارد و توسط مرورگر قابل درک است.
- در ابتدای بارگذاری برنامه، یک 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 سمت سرور برنامه نیز باید جهت کار و نمایش اطلاعات در سمت کلاینت در دسترس باشد؛ وگرنه قسمت‌های متناظر با آن، قادر به نمایش و یا ثبت اطلاعات نخواهند بود.
مطالب
مدیریت Instance در WCF
نحوه پیاده سازی و مدیریت Instance در پروژه‌های مبتنی بر WCF

نکته : آشنایی اولیه با مفاهیم WCF جهت درک صحیح مطالب الزامی است.

تشریح مسئله :  در صورتی که نیاز باشد که نمونه ساخته شده از سرویس (سمت سرور) به صورت Singleton  باشد بهترین روش برای پیاده سازی به چه صورت است.

برای شروع ابتدا مثال زیر را پیاده سازی می‌کنیم.
یک Contract به صورت زیر تعریف می‌کنیم:
[ServiceContract(SessionMode=SessionMode.Allowed)]
    public interface IMyService
    {
        [OperationContract]
        int GetData();             
    }

حالا یک سرویس برای پیاده سازی Interface بالا می‌نویسیم.
[ServiceBehavior( InstanceContextMode = InstanceContextMode.PerCall )]
    public class PerCallService : IMyService
    {
        int count;
        public int GetData()
        {
            return ++count;
        }
    }
همانطور که از نام سرویس مشخص است از این سرویس به ازای هر فراخوانی یک نمونه سمت سرور ساخته می‌شود.
حالا برای مشاهده نتیجه یک پروژه ConsoleApplication ایجاد کنید و سرویس مورد نظر را از روش AddServiceReference به پروژه اضافه کرده در فایل Program کد‌های زیر را کپی کنید.
 static void Main( string[] args )
        {
            Console.WriteLine( "PerCall Service" );

            MyPerCallService.MyServiceClient client = new MyPerCallService.MyServiceClient();
            int count = 0;
            for ( int i = 0 ; i < 5 ; i++ )
            {
                count = client.GetData();              
            }          
            Console.WriteLine( count );
            Console.ReadLine();         
        }
بعد از اجرا خروجی به صورت زیر است:

بعد از 5 بار فراخوانی متد GetData باز خروجی دارای مقدار 1 است. یعنی به ازای هر بار فراخوانی متد GetData یک نمونه از سرویس مورد نظر ساخته می‌شود.این عمل توسط خصوصیت InstanceContextMode که از نوع PerCall است به سرویس اعمال میشود.

حالا یک سرویس دیگر به صورت زیر ایجاد کنید.

 [ServiceBehavior( InstanceContextMode = InstanceContextMode.Single )]
    public class SingleService : IMyService
    {
        int count;
        public int GetData()
        {
            return ++count;
        }
    }
تنها تفاوت این سرویس با سرویس قبلی در این است که InstanceContextMode این سرویس  به صورت Single معرفی شده است. یعنی به ازای n فراخوانی فقط یک نمونه از کلاس ساخته می‌شود. این سرویس رو هم مثل روش قبلی به Client Application اضافه کنید.
کد کلاس Program رو به صورت زیر تغییر دهید.

static void Main( string[] args )
        {
            Console.WriteLine( "Single Service" );

            MySingleService.MyServiceClient client = new MySingleService.MyServiceClient();
            int count = 0;
            for ( int i = 0 ; i < 5 ; i++ )
            {
                count = client.GetData();              
            }          
            Console.WriteLine("Result is : {0}", count );
            Console.ReadLine();         
        }
که بعد از اجرا خروجی به صورت زیر است.

به ازای 5 بار فراخوانی سرویس متغیر Count سمت سرور مقدار قبلی خود را حفظ کرده است.

مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
یکی از ارکان لاینفک سیستم‌های سازمانی در هر پلتفرمی، چه وب و چه دسکتاپ و ... گزارش گیری از اطلاعات موجود و جزو ساختار حیاتی آن است. از آنجا که حتی ممکن است این گزارشات در هر دوره نیاز به تغییرات داشته باشند و گزارش‌های پویایی باشند؛ این نیاز احساس می‌شود که از یک برنامه گزارش ساز مناسب بهره ببریم. یکی از گزارش سازهای محبوب به خصوص در ایران که حتی نماینده رسمی آن هم در ایران وجود دارد، گزارش ساز StimulSoft Report است.

دارا بودن امکانات بسیار قدرتمند و پشتیبانی از محیط فارسی و همچنین پشتیبانی آن‌ها جهت پاسخگویی به سوالات، چه از طریق ایمیل یا چت، از نقاط قوت این ابزار به شمار می‌روند. در جدول مقایسات می‌توانید تفاوت نسخه‌های موجود این گزارش ساز را مشاهده کنید. برای استفاده در MVC از نسخه وب آن استفاده میکنیم.

در این مقاله قصد داریم با نحوه راه ندازی این ابزار در وب (MVC) آشنا شویم که شامل مباحث زیر می‌شود:
  1. استفاده از EF به عنوان منبع داده و ارسال آن‌ها به سمت گزارش ساز
  2. نحوه طراحی فایل MRT و بایند کردن داده‌های اطلاعاتی و ایجاد جدول
  3. استفاده از امکانات فایل خروجی ، چاپ و پیش نمایش و...
  4. بررسی Direction جهت استفاده در محیط‌های فارسی زبان
  5. نحوه ارسال اطلاعات بین دو اکشن متفاوت


طراحی فایل MRT

فایل MRT در واقع یک قالب (Template) خالی از مقادیر متغیر است که در StimulSoft Studio به طراحی آن میپردازیم و در برنامه خود، این مقادیر متغیر را با اطلاعات دلخواه خود جایگزین می‌کنیم. تصویر زیر یک نمونه از یک گزارش خالی است که ابتدا آن را طراحی کرده و سپس در برنامه آن را مورد استفاده قرار می‌دهیم:

برای اینکه فایل MRT بتواند دیتاهای لازمی را که به آن پاس میدهیم، بخواند و در جای مشخص شده قرار بدهد، باید یک BussinessObject برای آن ایجاد کنیم. بعد از اینکه یک گزارش جدید ایجاد کردید، در سمت راست به قسمت Dictionary بروید و در قسمت BussinessObject گزینه NewBussinessObject را انتخاب کنید. یک نام و نام مستعار که عموما هم یکی است، برای آن انتخاب کنید. در زیر همان پنجره شما می‌توانید ستون‌های اطلاعاتی خود را تعریف کنید. در اینجا من میخواهم اطلاعات یک راننده را به همراه خودروی وی، نشان دهم. برای همین، من دو موجودیت راننده و خودروی راننده را دارم. پس اسم Business Object را DriverReport میگذارم و ستون‌های اطلاعاتی فقط راننده (بدون در نظر گرفتن خودروی وی) را وارد میکنم.

در همین کادر بالا شما میتوانید تصیم بگیرید که آیا میخواهید اطلاعات خودرو را به همراه دیگرستون‌های اطلاعاتی راننده، ایجاد کنید یا اینکه برای خودرو یک نوع مجزا انتخاب کنید. اگر تنها یک خودرو برای راننده باشد، شاید راحت‌تر باشید همانند اطلاعات راننده با آن رفتار کنید. ولی اگر مثلا بخواهید خودروی‌های گذشته راننده را هم جز لیست داشته باشید، بهتر است یک  Business Object جدید متعلق و زیر مجموعه Business Object راننده ایجاد کنید. در اینجا چون تنها یک خودرو است، من آن اطلاعات آن را به همراه راننده، ارسال میکنم. شکل زیر ساختار درختی از گزارش بالاست:

شکل زیر هم یک ساختار دیگر از یک گزارش است که شامل Business object‌های مختلف می‌شود: 

سپس همین فیلدها را به سمت صفحه خالی بکشانید. با دو بار کلیک روی فیلدهای قرار گرفته در صفحه، با نحوه بایند کردن مقادیر آشنا می‌شوید؛ هر فیلدی که قرار است دیتای آن بایند شود، باید به شکل زیر در بخش Expression پنجره باز شده، نوشته شود:

{driverReport.LastName}
در صورتیکه قرار است Business Object به شکل یک لیست ارسال شود؛ مثلا لیست رانندگان یا حتی لیست خودروهایی که یک راننده از گذشته تا کنون داشته است، می‌توانید به جای درگ کردن فیلد به درون صفحه، خود Business Object را درگ کنید تا از روی آن یک جدول درست شود و با ارسال یک لیست به سمت آن و به ازای هر آیتم از این لیست یک سطر داشته باشید.

در دیکشنری همچنین انواع دیگری از فیلدها نیز به چشم میخورد:
متغیرها: این نوع فیلد یک متغیر است که به طور جداگانه میتواند مقداردهی شود و از آن بیشتر برای ارسال داده‌های تکی چون تصاویر، تاریخ شمسی و ... میتوان استفاده کرد.
متغیرهای سیستمی: این نوع متغیرها توسط خود گزارش ساز به طور مستقیم پر می‌شوند که شامل شماره صفحه، تاریخ و زمان، تعداد صفحات، مقادیر دو ارزشی (آیا صفحه آخر گزارش است؟) و ... می‌شود.
توابع: گزارش ساز شامل یک سری توابع آماده برای اعمال تغییرات بر روی داده‌ها میباشد که در دسته‌های مختلفی چون کار با رشته‌ها، زمان، ریاضیات و... قرار گرفته‌اند.
بعد از تکمیل آن، فایل MRT را ذخیره و در یک دایرکتوری در ساختار پروژه قرار دهید.

راه اندازی گزارش ساز در ASP.Net MVC

اولین کاری که می‌کنیم، ورود سه dll اصلی به پروژه است:

Stimulate.Base

Stimulate.Report

Stimulate.Report.MVC

در مرحله بعد یک متد ساخته و یک ویوو را برای صفحه گزارش گیری ایجاد می‌کنیم:

public ActionResult Report(int id)
{
   return View();
}
در ویوی مربوطه کدهای زیر را اضافه می‌کنیم:
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions()
{
   
    Localization = "~/content/reports/fa.xml",
    Actions =
        {
        
            GetReportSnapshot = "LoadReportSnapshot",
            ViewerEvent = "ViewerEvent",
            ExportReport = "ExportReport",
            PrintReport = "PrintReport",
        }
 }

در نسخه‌های دو سال اخیر، استفاده از این Helper تفاوت‌هایی در نحوه استفاده از خصوصیت‌های آن کرده است. در این روش جدید، پراپرتی‌ها دسته بندی شده و برای دسترسی به هر کدام باید به بخش آن مراجعه کنید؛ مثلا پراپرتی‌های Action، در دسته Actions قرار گرفته‌اند یا خصوصیت‌های ظاهری در دسته Appearance، یا گزینه‌های مرتبط با خروجی گرفتن‌ها، در دسته Export قرار گرفته‌اند و الی آخر که در نسخه‌های پیشین، کد بالا را به شکل زیر،  با پیشوند نام دسته می‌نوشتیم:
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions()
{
   
    Localization = "~/content/reports/fa.xml",
            ActionGetReportSnapshot = "LoadReportSnapshot",
            ActionViewerEvent = "ViewerEvent",
            ActionExportReport = "ExportReport",
            ActionPrintReport = "PrintReport",
            
}
خصوصیت GetReportSnapshot نام یک اکشن متد است که کار ارسال دیتا را به گزارش ساز، انجام میدهد. باقی خصوصیت‌ها را در ادامه بررسی میکنیم. پس متد LoadReportSnapshot را ایجاد می‌کنم و کدهای زیر را در ادامه آن می‌نویسیم:

بعد از آن لازم است دیتا‌ها را از طریق EF خوانده و به یک مدل جدید که بر اساس اطلاعات گزارش شماست و قرار است گزارش شما این پراپرتی‌ها را بشناسد، به طور دستی یا با استفاده یک کتابخانه mapping مثل automapper انتقال دهید. یا حتی می‌توانید مانند کد زیر از ساختاری ناشناس استفاده کنید. در کد زیر، من به صورت تمرینی اطلاعات یک راننده و خودروی او را انتقال میدهم:
var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                Model = "نام خودرو",
                MotorNumber = 415244,
                ProductionYear = 1394,
                Capacity = 4
                
};
اگر اطلاعات خودرو را هم به صورت مجزا BussinessObject ساخته‌اید باید آن را به شکل زیر تعریف کنید ( با فرض اینکه نام BussinessObject در گزارش، با نام car تعریف شده باشد):
var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                car = new
                {
                    Model = "نام خودرو",
                    MotorNumber = 415244,
                    ProductionYear = 1394,
                    Capacity = 4
                }
};
بعد از اینکه دیتاهای لازم را بر اساس فرمت دلخواه خود آماده کردیم، باید آن‌ها را به سمت گزارش ساز ارسال کنیم:
var report = new StiReport();
            report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
در مرحله اول یک وهله از شیء StiReport را می‌سازیم و فایل گزارشی را که در مرحله قبل ساختیم، به آن معرفی میکنیم. سپس داده‌های لازم را به آن انتقال می‌دهیم. پارامتر اول نام BussinessObject اصلی یعنی driverReport را وارد می‌کنیم و پارامتر دوم هم، همان اطلاعات گزارش است. خط بعدی هم یک متغیر است که من در گزارش تعریف کرده‌ام و در اینجا آن را با تاریخ شمسی امروز پر میکنم. توجه داشته باشید که انتقال اطلاعات حتما باید بعد از استفاده از متد Load باشد؛ در غیر اینصورت انتقالی انجام نخواهد شد. اینکه صرفا شما وهله‌ای از شیء StiReport بسازید و مقادیر را بدون ترتیب پر کنید، کفایت نمی‌کند. یعنی ترتیب زیر یک ترتیب کاملا اشتباه است:
var report = new StiReport();
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
           report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
چیزی که بعدا در خروجی می‌بینید، یک صفحه گزارش بدون مقدار است.
پس کد کامل ما برای ایجاد یک گزارش به شکل زیر می‌شود:
 public ActionResult LoadReportSnapshot()
{
  var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                Model = "نام خودرو",
                MotorNumber = 415244,
                ProductionYear = 1394,
                Capacity = 4

            };

            var report = new StiReport();
            report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
return StiMvcViewer.GetReportSnapshotResult(HttpContext, report);
}
همه خطوط، همان قبلی هاست که بررسی کردیم؛ بجز خط آخر که یک ActionResult اختصاصی است که در آن نحوه انتقال اطلاعات به گزارش ساز پیاده سازی شده است و تنها کاری که باید بکنیم این است که شیء گزارش ایجاد شده در بالا را به آن پاس دهیم.

اگر دوباره در ویو مربوطه، به سراغ helper برویم می‌بینیم که سه اکشن متد دیگر وجود دارند که در زیر، به ترتیب با نحوه کار آن‌ها و کد اکشن متد آن‌ها اشاره میکنیم:
Viewer Events : این اکشن متد که تنها یک خط ActionResult استاتیک را فراخوانی می‌کند، جهت مدیریت رویدادهای گزارش چون: زوم، صفحه بندی گزارش، خروجی‌ها و چاپ می‌باشد و وجود آن در گزارش از الزامات است.
 public virtual ActionResult ViewerEvent()
 {
            return StiMvcViewer.ViewerEventResult();
 }

PrintReport: برای مدیریت و ارسال گزارشات به دستگاه چاپ می‌باشد. این اطلاعات از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط PrintReportResult آن را دریافت می‌کند.
public virtual ActionResult PrintReport()
        {
            return StiMvcViewer.PrintReportResult(this.HttpContext);
        }

ExportReport: گزارش ساز استیمول به شما اجاز میدهد در فرمت‌های گوناگونی چون xlsx,docx,pptx,pdf,rtf و ... از گزارش خود خروجی بگیرید. اطلاعات گزارش از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط ExportReportResult  دریافت می‌شود. 
 public virtual ActionResult ExportReport()
        {
            return StiMvcViewer.ExportReportResult(this.HttpContext);
        }
حال اگر برنامه را اجرا کنید، باید گزارشی به شکل زیر نمایش داده شود و مقادیر در جای خود شکل گرفته باشند. ولی مشکلی  که ممکن است این گزارش داشته باشد این است که برای فارسی، حالت راست به چپ را ندارد.


 البته خوشبختانه این مشکل  در حالت پیش نمایش و چاپ و خروجی‌ها دیده نمی‌شود و فقط مختص نمایش روی فرم Html است. برای حل این مشکل ممکن است از گزینه یا پراپرتی RightToLeft، در بخش Appearance موجود در helper استفاده کنید که البته استفاده از آن مانند تصویر بالا، فقط محدود به container گزارش و نوار ابزار آن می‌شود. برای حل این مشکل کافی است کد css زیر را به صفحه گزارش اضافه کنید تا مشکل حل شود:
.stiMvcViewerReportPanel table{
    direction:ltr !important;
}
مجددا گزارش را ایجادکنید تا گزارش را به طور صحیحی مشاهده کنید:

حال حتما پیش خود میگویید که این روش برای اطلاعات ایستا و تمرینی مناسب است و من چگونه باید پارامترهای ارسالی به اکشن متد Report را به اکشن متد LoadReportSnapshot ارسال کنم. برای این منظور استفاده از SessionState‌ها زیاد توصیه شده‌است:
 public virtual ActionResult Report(int id)
        {
            TempData["id"]=id;
            return View();
        }

         public virtual ActionResult LoadReportSnapshot()
        {
            var driverId = (int)TempData ["id"];
              //.....
        }
  ولی این روش مشکلات زیادی را دارد. اول اینکه اگر کاربر چند گزارش جداگانه را پشت سر هم باز کند، به دلیل اینکه گزارش مدتی طول می‌کشد باز شود، همه گزارش‌ها آخرین گزارش درخواستی خواهند بود و دوم اینکه مقداری از حافظه سرور را هم بی جهت اشغال میکند. ولی برای کار با استیمول به هیچ یک از این کارها نیازی نیست، چون خود استیمول به طور خودکار پارامترهای ارسالی را انتقال می‌دهد. یعنی کد باید به شکل زیر نوشته شود:
public virtual ActionResult Report(int id)
        {
    
            return View();
        }

         public virtual ActionResult LoadReportSnapshot(int id)
        {
             //.....
        }
همین مقدار کد برای ارسال پارمترها کفایت میکند و مابقی کار را به stimul بسپارید.

نکته بسیار مهم: گزارش ساز استیمول متاسفانه شامل تنظیم پیش فرض نامناسبی است که عملیات کش را بر روی گزارش‌ها اعمال می‌کند. به عنوان مثال تصور کنید من صفحه گزارش شخصی به نام «وحید نصیری» را باز میکنم و در تب دیگر گزارش شخص دیگری با نام «علی یگانه مقدم» را باز میکنم. حال اگر کاربر به سراغ تب آقای نصیری برود و بخواهد چاپ یا خروجی درخواست کند، اشتباها با گزارش علی یگانه مقدم روبرو خواهد شد که این اتفاق به دلیل کش شدن رخ میدهد. برای غیر فعال کردن این قابلیت پیش فرض، کد زیر را در Helper اضافه کنید:
Server =
    {
        GlobalReportCache = false
    }
مطالب
گوش دادن به تغییرات تم ویندوز 10 بدون نیاز به WinRT در سی شارپ
امروز میخواستم برای یکی از پروژه‌هایم، قابلیتی را پیاده سازی کنم که هماهنگ با تم ویندوز، تم برنامه را عوض کند (تیره/روشن). به این منظور که وقتی تم ویندوز Dark می‌شد، تم برنامه‌ی من هم Dark بشود و برعکس. ساده‌ترین کار این بود که از کدهای WinRT که توسط بسته‌ی نیوگت SDK Contract ارائه میشود استفاده کرد. در این صورت کافیست فقط از کلاس ThemeManager استفاده کنیم و بدون کوچکترین خونریزی، برنامه را به این ویژگی مجهز کنیم😁 اما خب، هرچیزی هزینه‌ی خودش را دارد و من به شخصه علاقه‌ای به استفاده از 25 مگابایت، فقط برای شناسایی وضعیت تم ویندوز را ندارم! پس خودم دست به کار شدم تا یک Listener برای این منظور بنویسم.
در دات نت یکسری رخ‌داد وجود دارند که مربوط به سیستم عامل میشوند و از کلاس SystemEvents قابل دسترسی هستند. در اینجا ایونتی داریم به اسم UserPreferenceChanged که شامل مواردی میشود که کاربر، تنظیمات ویندوز را تغییر می‌دهد. هر تغییری که در تنظیمات ویندوز اعمال بشود، درون یکی از Category‌ها صدا زده میشود. پس اگر ما این ایونت را رجیستر کنیم، هر موقع تغییری در تنظیمات ویندوز اعمال بشود، برنامه‌ی ما نیز متوجه میشود.
برای این منظور یک کلاس را ایجاد می‌کنیم و در متد سازنده‌ی آن، این رخ‌داد را ثبت میکنیم:
pubic class ThemeHelper
{
    public ThemeHelper()
    {
        SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;
    }

    private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
    {
        switch (e.Category)
        {
            case UserPreferenceCategory.Accessibility:
                break;
            case UserPreferenceCategory.Color:
                break;
            case UserPreferenceCategory.Desktop:
                break;
            case UserPreferenceCategory.General:
                break;
            case UserPreferenceCategory.Icon:
                break;
            case UserPreferenceCategory.Keyboard:
                break;
            case UserPreferenceCategory.Menu:
                break;
            case UserPreferenceCategory.Mouse:
                break;
            case UserPreferenceCategory.Policy:
                break;
            case UserPreferenceCategory.Power:
                break;
            case UserPreferenceCategory.Screensaver:
                break;
            case UserPreferenceCategory.Window:
                break;
            case UserPreferenceCategory.Locale:
                break;
            case UserPreferenceCategory.VisualStyle:
                break;
        }
    }
}
 همینطور که می‌بینید، این دسته بندی شامل موارد مختلفی میشود که به بخش‌های مختلف تنظیمات ویندوز مربوط است. تنظیمات مربوط به تم، درون General صدا زده میشود. پس کدهای ما قرار است وارد این قسمت بشود. متاسفانه این رخ‌داد اطلاعات کاملتری را به ما نمی‌دهد و فقط اطلاع می‌دهد که تغییری رخ داده (همینطور که قبلا گفتم، هرچیزی هزینه‌ای دارد). پس ما باید مشخصات تم فعلی را دریافت کنیم؛ اما از کجا؟ معلوم است رجیستری! همه چیز در رجیستری ثبت میشود و به‌راحتی قابل دسترسی هست. پس یک متد می‌نویسیم که کلید رجیستری مربوطه را بخواند و آن را بصورت یک مدل (Light یا Dark) برگرداند:   
    private const string RegistryKeyPathTheme = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
    private const string RegSysMode = "SystemUsesLightTheme";

    public static UITheme GetWindowsTheme()
    {
        return GetThemeFromRegistry(RegSysMode);
    }

    private static UITheme GetThemeFromRegistry(string registryKey)
    {
        using var key = Registry.CurrentUser.OpenSubKey(RegistryKeyPathTheme);
        var themeValue = key?.GetValue(registryKey) as int?;
        return themeValue != 0 ? UITheme.Light : UITheme.Dark;
    }
    
    public enum UITheme
    {
        Light,
        Dark
    }
حالا ما نیاز به یک رخ‌داد داریم که کاربر بتواند در برنامه‌ی خودش آن‌را ثبت کند و نیازی به پیاده سازی این کدها نداشته باشد. پس یک EventHandler را به اسم WindowsThemeChanged ایجاد میکنیم: 
    public event EventHandler<FunctionEventArgs<UIWindowTheme>> WindowsThemeChanged;
    
    protected virtual void OnWindowsThemeChanged(UIWindowTheme theme)
    {
        EventHandler<FunctionEventArgs<UIWindowTheme>> handler = WindowsThemeChanged;
        handler?.Invoke(this, new FunctionEventArgs<UIWindowTheme>(theme));
    }
من میخواهم که کاربر، مقدار تم فعلی و رنگ Accent فعلی را نیز بتواند از طریق این رخ‌داد، دریافت کند. پس ما باید یک EventArgs را ایجاد کنیم که پراپرتی‌های دلخواهی را داشته باشد. برای همین کلاس FunctionEventArgs را ایجاد میکنیم:
public class FunctionEventArgs<T> : RoutedEventArgs
{
    public FunctionEventArgs(T theme)
    {
        Theme = theme;
    }
    public FunctionEventArgs(RoutedEvent routedEvent, object source) : base(routedEvent, source) { }
    public T Theme { get; set; }
}
این کلاس از RoutedEventArgs ارث بری کرده و بصورت جنریک پیاده سازی شده‌است. به این معنا که ما میتوانیم هر نوع دلخواهی را که خواستیم، به عنوان arg استفاده کنیم. اگر دقت کنید من یک مدل دلخواه را به عنوان arg مشخص کرده‌ام:
FunctionEventArgs<UIWindowTheme>
میتوانستیم از همان UITheme هم استفاده کنیم؛ ولی نمی‌توانستیم مقدار Accent را به کاربر برگردانیم. برای همین، مدلی را به اسم UIWindowTheme ایجاد میکنیم:
public class UIWindowTheme
{
    public Brush AccentBrush { get; set; }
    public UITheme CurrentTheme { get; set; }
}
کار تمام است. حالا باید کدهای داخل متد SystemEvents_UserPreferenceChanged را بنویسیم (دقت کنید که کد، باید داخل بخش General نوشته شود):
case UserPreferenceCategory.General:
   var changedTheme = new UIWindowTheme()
   {
         AccentBrush = SystemParameters.WindowGlassBrush,
         CurrentTheme = GetWindowsTheme()
   };
   OnWindowsThemeChanged(changedTheme);
break;
یک مدل را ایجاد کرده و مقدار AccentBrush را برابر با WindowGlassBrush قرار می‌دهیم. این پراپرتی هم مانند ایونتی که اول معرفی کردیم، مربوط به سیستم عامل بوده و رنگ فعلی Accent را بر می‌گرداند. برای مقدار CurrentTheme نیز متدی را که بالاتر برای دریافت تم فعلی از رجیستری نوشتیم، صدا می‌زنیم و در پایان این مدل را به ایونت، پاس می‌دهیم. در پایان می‌توانیم به این صورت ایونت خود را پیاده سازی کنیم:
    ThemeHelper tm = new ThemeHelper();
    tm.WindowsThemeChanged +=OnWindowsThemeChanged;

    private void OnWindowsThemeChanged(object? sender, FunctionEventArgs<RegistryThemeHelper.UIWindowTheme> e)
    {
        rec.Fill = e.Theme.AccentBrush;
        if (e.Theme.CurrentTheme == ThemeHelper.UITheme.Light)
        {
            Background = Brushes.White;
        }
        else
        {
            Background = Brushes.Black;
        }
    }

 

  
مطالب
توسعه سرویس‌های مبتنی بر REST در AngularJs با استفاده از RestAngular : بخش دوم
در بخش پیشین  کلیات کتابخانه‌ی Restangular  را بررسی کردیم. در این بخش قصد داریم تا در طی یک پروژه، امکانات و قابلیت‌های بی‌نظیر این سرویس را در یک پروژه‌ی واقعی مشاهده کنیم.

کلیات پروژه

در این پروژه قصد داریم تا لیست کتاب‌های یک کتابخانه را نمایش دهیم. این کتابها قابلیت ویرایش نام دارند و همچنین شما می‌توانید کتابهای جدیدی را به لیست کتابها اضافه نمایید. تصویر زیر خروجی این پروژه است:

پایگاه داده‌ی برنامه با نام LibDb درون پوشه‌ی app_data قرار داده شده‌است. این پایگاه داده تنها دارای یک جدول است؛ با نام Books که دیاگرام آن را در شکل زیر مشاهده می‌کنید:


پیاده سازی

در ابتدا یک پروژه‌ی Empty را با رفرنس web API ایجاد می‌کنیم. حال درون فایل WebApiConfig.cs تکه کد زیر را اضافه می‌کنیم. این تکه کد فیلدها و آبجکت‌هایی را که از سمت سرور بازگشت داده می‌شوند، به صورت camelCase تبدیل می‌کند.
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();

jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
در ادامه مدل edmx را ساخته و پس از آن Books Web Api را درون پوشه‌ی کنترلر ایجاد می‌کنیم. این کنترلر به صورت Default web Api Scaffold ساخته شده است و به دلیل اینکه به بحث ما مرتبط نیست؛ از توضیح بیشتر آن می‌گذریم.
حال پوشه‌ای را با نام app برای نگه داری فایل‌های AngularJs اضافه می‌کنیم و درون آن فایل app.js را ایجاد می‌کنیم:
var angularExample = angular.module('angularexample', ["restangular"])
angularExample.config(["RestangularProvider", function (RestangularProvider) {
    //RestangularProvider.setRestangularFields({
    //    id: "id"
    //});
    RestangularProvider.setBaseUrl('/api');
}]);

angularExample.controller("MainCtrl", ["Restangular", "$scope", function (Restangular, $scope) {
    var resource = Restangular.all('books');
    resource.getList().then(function (response) {
        $scope.books = response;
    });
    $scope.add = function () {
        resource.post($scope.newBook).then(function (newResource) {
            $scope.books.push(newResource);
        })
    }
}]);
محتویات فایل app.js را مشاهده می‌کنید. در ادامه درباره‌ی این قسمت بیشتر صحبت می‌کنیم.
حال در روت پروژه فایل index.html را ایجاد می‌کنیم:
<!DOCTYPE html>
<html ng-app="angularexample" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Rest Angular Sample</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <script src="http://cdn.jsdelivr.net/underscorejs/1.5.1/underscore-min.js"></script>
    <script src="/Scripts/restangular.min.js"></script>
    <script src="/app/app.js"></script>
</head>
<body>
    <div ng-controller="MainCtrl">
        <div ng-repeat="item in books">
            name is: {{item.name}}<br />
            change name: <input type="text" ng-model="item.name" /><button type="submit" ng-click="item.put();">update</button>
        </div>
        <div>
            add new: <br />
            <input type="text" ng-model="newBook.name" /><button type="submit" ng-click="add()">add</button>
        </div>
    </div>
</body>
و در نهایت در پوشه‌ی Scripts فایل‌های سرویس Restangular را قرار می‌دهیم.
تا به اینجای کار تمامی کارهای مورد نیاز تشکیل پروژه را انجام داده‌ایم. حال به بررسی بیشتر سرویس‌های این پروژه می‌پردازیم؛ یعنی کدهای درون فایل app.js.
ببینیم که برای دریافت لیست تمامی کتابها، ما چه کارهایی را انجام داده‌ایم! به تکه کد زیر دقت کنید:
    var resource = Restangular.all('books');
    resource.getList().then(function (response) {
        $scope.books = response;
    });
این تمامی آن چیزی است که ما برای دریافت لیست تمامی کتابها انجام داده‌ایم. به همین سادگی! در خط اول از متد all که در بخش قبل توضیح مختصری راجع به آن داده بودم برای دریافت لیست تمامی کتابها استفاده کردم که پارامتر درون آن آدرس Web Api است. البته همانطور که می‌دانید در ASP.NET Web Api ما همیشه به base address یک api نیز اضافه می‌کنیم. حال اینکه چرا api در اینجا نیامده در ادامه و در بخش تنظیمات کلی Restangular توضیح می‌دهم. در خط دوم نیز از اشاره‌گر resources متد getList را فراخوانی کرده و لیست کتابها را در response دریافت می‌کنیم.
پس از آن می‌خواهیم ببینیم که عملیات ایجاد یک کتاب جدید چگونه انجام می‌گردد. تکه کد زیر این عملیات را انجام می‌دهد:
    $scope.add = function () {
        resource.post($scope.newBook).then(function (newResource) {
            $scope.books.push(newResource);
        })
    }
ما یک متد با نام add ایجاد کرده‌ایم که در سمت View توسط دایرکتیو ng-click آن را فراخوانی می‌کنیم.
همانطور که مشاهده میکنید درون app.js متدی برای update موارد قبلی نیست. بیایید سری به View بزنیم. به المنت div زیر توجه کنید. همانطور که می‌بینید تمامی عملیات update با یک دستور ساده item.put حل و فصل شده.
        <div ng-repeat="item in books">
            name is: {{item.name}}<br />
            change name: <input type="text" ng-model="item.name" /><button type="submit" ng-click="item.put();">update</button>
        </div>
تمامی آنچه که گفته شد تنها بخشی از قابلیت‌های شگفت انگیز این افزونه بود. امیدوارم که مطلب مفید واقع شده باشد. سورس این پروژه را می‌توانید از لینک زیر دریافت کنید.

سورس پروژه: RestangularSample.rar  

مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور
تا اینجا تنظیمات اصلی فرم ثبت اطلاعات کارمندان را انجام دادیم. اکنون نوبت به ارسال این اطلاعات به سمت سرور است. پیشنیاز آن نیز تدارک مواردی است که در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» پیشتر بحث شدند. از این مطلب تنها تنظیمات موارد ذیل را نیاز خواهیم داشت و از تکرار آن‌ها در اینجا صرفنظر می‌شود تا هم مطلب کوتاه‌تر شود و هم بتوان بر روی اصل موضوع جاری، تمرکز کرد:
- ایجاد یک پروژه‌ی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامه‌ی ASP.NET Core خالی برای اجرای یک برنامه‌ی Angular CLI
- تنظیمات فایل آغازین یک برنامه‌ی ASP.NET Core جهت ارائه‌ی برنامه‌های Angular
- ایجاد ساختار اولیه‌ی برنامه‌ی Angular CLI در داخل پروژه‌ی جاری: این مورد را تاکنون انجام داده‌ایم و تکمیل کرده‌ایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشه‌ی angular-template-driven-forms-lab (پروژه‌ی این سری) به ریشه‌ی پروژه‌ی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشه‌ی wwwroot
- روش اول و یا دوم اجرای برنامه‌های مبتنی بر ASP.NET Core و Angular CLI

البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز می‌توانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامه‌ی Angular و ASP.NET Core را می‌توان توسط VSCode به خوبی مدیریت و اجرا کرد.


ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور

در برنامه‌های Angular مرسوم است جهت کاهش مسئولیت‌های یک کلاس و امکان استفاده‌ی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاس‌های کامپوننت‌ها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آن‌را انجام می‌دهیم.
ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا می‌کنیم:
 >ng g s employee/FormPoster -m employee.module
با این خروجی
 installing service
  create src\app\employee\form-poster.service.spec.ts
  create src\app\employee\form-poster.service.ts
  update src\app\employee\employee.module.ts
همانطور که در سطر آخر نیز ملاحظه می‌کنید، فایل employee.module.ts را جهت درج کلاس جدید FormPosterService در قسمت providers ماژول آن به روز رسانی می‌کند؛ تا بتوانیم این سرویس را در کامپوننت‌های این ماژول تزریق کرده و استفاده کنیم.
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر می‌دهیم:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

import { Employee } from './employee';

@Injectable()
export class FormPosterService {

    constructor(private http:Http) {
    }

    postEmployeeForm(employee: Employee) {
    }
}
در اینجا سرویس Http انگیولار به سازنده‌ی کلاس تزریق شده‌است و این نحوه‌ی تعریف سبب می‌شود تا بتوان به پارامتر http، به صورت یک فیلد خصوصی تعریف شده‌ی در سطح کلاس نیز دسترسی پیدا کنیم.
چون این کلاس از ماژول توکار Http استفاده می‌کند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
import { HttpModule } from "@angular/http";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    EmployeeModule,
    AppRoutingModule
  ]
اکنون می‌توانیم این سرویس جدید FormPosterService را به سازنده‌ی کامپوننت EmployeeRegisterComponent در فایل src\app\employee\employee-register\employee-register.component.ts تزریق کنیم:
import { FormPosterService } from "../form-poster.service";

export class EmployeeRegisterComponent implements OnInit {

  constructor(private formPoster: FormPosterService) {}

}

در ادامه برای آزمایش برنامه، به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی، دستورات:
>npm install
>ng build --watch
و در دومی، دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
دستورات اول کار بازیابی وابستگی‌های سمت کلاینت و سپس ساخت تدریجی برنامه‌ی Angular را دنبال می‌کند. دستورات دوم، وابستگی‌های برنامه‌ی ASP.NET Core را دریافت و نصب کرده و سپس برنامه را در حالت watch ساخته و بر روی پورت 5000 ارائه می‌کند (بدون نیاز به اجرای VS 2017؛ این دستور عمومی است).
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگه‌ی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.


مدیریت رخداد submit فرم در Angular

تا اینجا کار برپایی تنظیمات اولیه‌ی کار با سرویس Http را انجام دادیم. مرحله‌ی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
در حین رخدادگردانی submit می‌توان به template reference variable تعریف شده‌ی form# برای دسترسی به وهله‌ای از ngForm نیز کمک گرفت.
export class EmployeeRegisterComponent implements OnInit {
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
امضای متد submitForm را در اینجا مشاهده می‌کنید. form دریافتی آن از نوع NgForm است که در ابتدای فایل import شده‌است.
در همین حال اگر بر روی دکمه‌ی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر می‌توان مشاهده کرد:


اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کرده‌است. همانطور که مشاهده می‌کنید، مقدار form.value بسیار شبیه است به وهله‌ای از مدلی که در سطح کلاس تعریف کرده‌ایم و این مقدار همواره توسط Angular نگهداری و مدیریت می‌شود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرم‌های مبتنی بر قالب‌ها به صورت جداگانه‌ای تهیه کرد. توسط شیء form نیز می‌توان به تمام اطلاعات فیلدها دسترسی یافت.


تکمیل سرویس ارسال اطلاعات به سرور

در ادامه می‌خواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل می‌کنیم:
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 { Employee } from "./employee";

@Injectable()
export class FormPosterService {
  private baseUrl = "api/employee";

  constructor(private http: Http) {}

  private extractData(res: Response) {
    const body = res.json();
    return body.fields || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  postEmployeeForm(employee: Employee): Observable<Employee> {
    const body = JSON.stringify(employee);
    const headers = new Headers({ "Content-Type": "application/json" });
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(this.baseUrl, body, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
برای کار با Observables یا می‌توان نوشت 'import 'rxjs/Rx که تمام بسته‌ی RxJS را import می‌کند، یا همانند این مثال بهتر است تنها اپراتورهایی را که به آن‌ها نیاز پیدا می‌کنیم، import نمائیم. به این ترتیب حجم نهایی ارائه‌ی برنامه نیز کاهش خواهد یافت.
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode می‌شود. البته متد post اینکار را به صورت توکار نیز می‌تواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمی‌داند که چه نوع فرمتی را دریافت کرده‌است و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده می‌شود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل می‌کنیم:
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);

    // validate form
    this.validatePrimaryLanguage(this.model.primaryLanguage);
    if (this.hasPrimaryLanguageError) {
      return;
    }

    this.formPoster
      .postEmployeeForm(this.model)
      .subscribe(
        data => console.log("success: ", data),
        err => console.log("error: ", err)
      );
  }
در اینجا ابتدا اعتبارسنجی سفارشی drop down را که در قسمت قبل بررسی کردیم، قرار داده‌ایم. پس از آن متد postEmployeeForm سرویس formPoster فراخوانی شده‌است و در اینجا کار subscribe به نتیجه‌ی عملیات صورت گرفته‌است که می‌تواند حاوی اطلاعاتی از سمت سرور و یا خطایی در این بین باشد.

یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.


تکمیل Web API برنامه‌ی ASP.NET Core جهت دریافت اطلاعات از کلاینت‌ها

در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
export class FormPosterService {
  private baseUrl = "api/employee";
به همین جهت نیاز است سرویس Web API سمت سرور خود را بر این مبنا تکمیل کنیم.
ابتدا مدل زیر را به پروژه‌ی ASP.NET Core جاری، معادل نمونه‌ی تایپ‌اسکریپتی سمت کلاینت آن اضافه می‌کنیم. البته در اینجا یک Id نیز اضافه شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool IsFullTime { get; set; }
        public string PaymentType { get; set; }
        public string PrimaryLanguage { get; set; }
    }
}

سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
using Microsoft.AspNetCore.Mvc;
using AngularTemplateDrivenFormsLab.Models;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class EmployeeController : Controller
    {
        public IActionResult Post([FromBody] Employee model)
        {
            //todo: save model

            model.Id = 100;
            return Created("", new { fields = model });
        }
    }
}
این کنترلر با شیوه‌ی Web API تعریف شده‌است. مسیریابی آن با api شروع می‌شود تا با مسیر baseUrl سرویس formPoster تطابق پیدا کند.
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه می‌کنید، بازگشت داده شده‌است. این نوع خروجی، یک چنین JSON ایی را تولید می‌کند:
{"fields":{"id":100,"firstName":"Vahid","lastName":"N","isFullTime":true,"paymentType":"FullTime","primaryLanguage":"Persian"}}
به همین جهت است که در متد extractData، دسترسی به body.fields را مشاهده می‌کنید. این fields در اینجا دربرگیرنده‌ی اطلاعات بازگشتی از سرور است (نام آن دلخواه است و درصورت تغییر آن در سمت سرو، باید این نام را در متد extractData نیز اصلاح کنید).
  private extractData(res: Response) {
    const body = res.json();
    return body.fields || {};
  }
اکنون اگر برنامه را با دستورات dotnet watch build و ng build --watch اجرا کنیم، بر روی پورت 5000 قابل دسترسی خواهد بود و پس از ارسال فرم به سرور، چنین خروجی را می‌توان در کنسول developer مرورگر مشاهده کرد:


نمایش success به همراه شیءایی که از سمت سرور دریافت شده‌است؛ که حاصل اجرای سطر ذیل در متد submitForm است:
 data => console.log("success: ", data)
همانطور که مشاهده می‌کنید، این شیء به همراه Id نیز هست. بنابراین درصورت نیاز به آن در سمت کلاینت، خاصیت معادل آن‌را به کلاس کارمند اضافه کرده و در همین سطر فوق می‌توان به آن دسترسی یافت.


بارگذاری اطلاعات drop down از سرور

تا اینجا اطلاعات drop down نمایش داده شده از یک آرایه‌ی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آن‌ها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
[HttpGet("/api/[controller]/[action]")]
public IActionResult Languages()
{
    string[] languages = { "Persian", "English", "Spanish", "Other" };
    return Ok(languages);
}
که برای آزمایش آن می‌توانید مسیر http://localhost:5000/api/employee/languages را جداگانه در مرورگر درخواست کنید.
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه می‌کنیم که کار آن‌ها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
  private extractLanguages(res: Response) {
    const body = res.json();
    return body || {};
  }

  getLanguages(): Observable<any> {
    return this.http
      .get(`${this.baseUrl}/languages`)
      .map(this.extractLanguages)
      .catch(this.handleError);
  }
اینبار چون خروجی سمت سرور را مانند قبل (متد extractData) داخل فیلدی مانند fields محصور نکردیم، همان body دریافتی بازگشت داده شده‌است.
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
  languages = [];

  ngOnInit() {
    this.formPoster
      .getLanguages()
      .subscribe(
        data => this.languages = data,
        err => console.log("get error: ", err)
      );
  }
ابتدا آرایه‌ی زبان‌ها با یک آرایه‌ی خالی مقدار دهی شده‌است و سپس در متد ngOnInit، کار دریافت اطلاعات آن از سرور، صورت گرفته‌است.

مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت می‌توان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
<h3 *ngIf="languages.length == 0">Loading...</h3>
<div class="container" *ngIf="languages.length > 0">
در این حالت هر زمانیکه آرایه‌ی زبان‌ها پر شد، loading حذف شده و div نمایان می‌گردد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات:
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت ششم - کامپوننت‌های تو در تو
گاهی از اوقات جهت refactoring یک template بزرگ، بهتر است آن‌را به چند template کوچک خرد کرد و سپس از جمع آن‌ها به صورت یک template اصلی استفاده نمود. در این حالت نیاز است بین این زیر کامپوننت‌ها و کامپوننت‌های دربرگیرنده‌ی آن‌ها ارتباطات لازم را برقرار کرد.
تا اینجا در قسمت سوم، نحوه‌ی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کننده‌ی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننت‌ها ارتباط برقرار کرد؟


تهیه کامپوننت نمایش ستاره‌ای امتیازهای محصولات

مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت می‌خواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستاره‌ای آن‌ها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرنده‌ی آن دریافت کند. همچنین می‌خواهیم اگر کاربر بر روی این ستاره‌ها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شده‌است:
 <td>{{ product.starRating }}</td>
البته می‌توان در همینجا کدهای نمایش ستاره‌ای را بجای درج مقدار عددی آن قرار داد، اما ... قالب جاری را بیش از اندازه شلوغ خواهد کرد. به همین دلیل بهتر است نمایش آن‌را تبدیل به یک کامپوننت مجزا کرد. به علاوه در این حالت، قابلیت استفاده‌ی مجدد از آن در سایر کامپوننت‌ها نیز وجود خواهد داشت.
با توجه به اینکه کامپوننت نمایش ستاره‌ای امتیازها، قابلیت استفاده‌ی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آن‌را به خارج از پوشه‌ی products فعلی منتقل کنیم. برای مثال می‌توان پوشه‌ی app\shared را برای آن و تمامی کامپوننت‌های با قابلیت استفاده‌ی مجدد ایجاد کرد.


برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core';
 
@Component({
    selector: 'ai-star',
    templateUrl: 'app/shared/star.component.html',
    styleUrls: ['app/shared/star.component.css']
})
export class StarComponent implements OnChanges {
    @Input() rating: number;
    starWidth: number;
    @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
 
    ngOnChanges(): void {
        this.starWidth = this.rating * 86 / 5;
    }
 
    onClick() {
        this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
    }
}
روش ساخت این کامپوننت نیز همانند سایر کامپوننت‌ها است و اصول کلی آن تفاوتی نمی‌کند. در اینجا نیز کلاسی وجود دارد که export شده و همچنین به Component مزین است. مقدار selector آن نیز به ai-star تنظیم شده‌است.
سپس مسیر template و مسیر فایل css ویژه‌ی آن، در تزئین کننده‌ی Component مشخص شده‌اند. محتوای کامل این دو فایل را در ذیل مشاهده می‌کنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop"
     [style.width.px]="starWidth"
     [title]="rating"
     (click)='onClick()'>
    <div style="width: 86px">
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
    </div>
</div>
ب) محتوای فایل App\shared\star.component.css
.crop {
    overflow: hidden;
}
 
div {
    cursor: pointer;
}
قالب star.component.html کار نمایش پنج ستاره را انجام می‌دهد. عرض کلی آن بر اساس مقدار خاصیت starWidth مشخص می‌شود و بر همین اساس، تعداد نمایان ستاره‌ها، مشخص خواهند شد. خاصیت starWidth به width این div بر حسب px، متصل شده‌است (property binding). همچنین خاصیت title این div نیز به مقدار rating متصل شده‌است و اگر بر روی آن کلیک شود، متد onClick را در کلاس متناظر با کامپوننت خود، فراخوانی خواهد کرد (event binding).

معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده می‌شود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه می‌شود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستاره‌ها است.


مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد

متد ngOnChanges، تنها به خواص ویژه‌ای به نام «input properties» واکنش نشان می‌دهد. اگر یک کامپوننت تو در توی قرار گرفته‌ی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژه‌ای به نام ()Input@ انجام می‌شود.
به همین جهت است که پیش از خاصیت rating در کلاس  StarComponent، شاهد درج مزین کننده‌ی ویژه‌ی ()Input@ هستیم:
export class StarComponent implements OnChanges {
      @Input() rating: number;
مزین کننده‌ی ()Input@ را به هر خاصیتی با هر نوعی، می‌توان انتساب داد. در اینجا نیاز است کامپوننت نمایش ستاره‌ای امتیازها، عدد امتیاز را از والد خود دریافت کنند. به همین جهت خاصیت امتیاز را از نوع «خاصیت ورودی» مشخص کرده‌ایم.
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی می‌کند:
 [rating]='product.starRating'

بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشده‌است، قابلیت property binging فوق را در کامپوننت والد، ندارد.

اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که می‌توان از این مقدار تغییر یافته، جهت ترجمه‌ی آن به عرض div نمایش ستاره‌ها، استفاده کرد.


ارسال داده‌ها از کامپوننت فرزند به کامپوننت والد

تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژه‌ی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا می‌کند:
export class StarComponent implements OnChanges {
    @Input() rating: number;
    starWidth: number;
    @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در اینجا خاصیت ratingClicked را که با ()Output@ مزین شده‌است، مشاهده می‌کنید. نوع این خاصیت باید از نوع رخدادها باشد که در AngularJS 2.0 توسط شیء EventEmitter جنریک، تعریف شده‌است. در این مثال، نوع رخداد و مقداری که توسط آن ارسال می‌شود، رشته‌ای درنظر گرفته شده‌است. اگر نیاز به ارسال چندین مقدار بود، می‌توان یک شیء را در اینجا مشخص کرد.
در مثال جاری اگر کاربر بر روی div ستاره‌های نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام می‌شود (متد جدید onClick به رخداد click متصل شده‌است):
<div class="crop"
     [style.width.px]="starWidth"
     [title]="rating"
     (click)='onClick()'>
و سپس این رخداد کلیک، در کلاس StarComponent به نحو ذیل به والد خود منتقل خواهد شد:
onClick() {
     this.ratingClicked.emit(`The rating ${this.rating} was clicked!`);
 }
در اینجا متد emit است که اطلاعات را به کامپوننت دربرگیرنده، ارسال می‌کند. نحوه‌ی تعریف این رشته هم توسط back tick مربوط به ES 6 صورت گرفته‌است.

تا اینجا مرحله‌ی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کننده‌ی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.

اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شده‌ی با Output، به عنوان مقصد event binding مشخص می‌شود و سپس متدی در کلاس کامپوننت والد، به آن متصل می‌گردد:
 (ratingClicked)='onRatingClicked($event)'
event$ مقدار ارسالی از کامپوننت فرزند را به کامپوننت والد منتقل می‌کند.
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه می‌کنیم:
onRatingClicked(message: string): void {
    this.pageTitle = 'Product List: ' + message;
}
نوع پارامتر ورودی این متد را با توجه به نوع رشته‌ای <EventEmitter<string در کلاس StarComponent  تعریف کردیم.
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستاره‌ای امتیازها، خاصیت pageTitle درج شده‌ی در صفحه تغییر می‌کند.


استفاده از کامپوننت نمایش ستاره‌ای امتیازها

نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شده‌ی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشه‌ی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر می‌دهیم:
<td>
    <ai-star [rating]='product.starRating'
             (ratingClicked)='onRatingClicked($event)'>
    </ai-star>
</td>
همانطور که ملاحظه می‌کنید، ابتدا selector این کامپوننت جدید، به صورت یک المان جدید و سفارشی HTML، در قالب کامپوننت لیست محصولات درج می‌شود. همچنین خاصیت rating ورودی آن به عدد امتیاز محصول جاری در حال رندر، متصل شده‌است. به علاوه توسط event binding به خاصیت ratingClicked که از نوع ()Output@ تعریف شده‌است، متد onRatingClicked متصل گردیده‌است.

سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی می‌کنیم:
import { Component, OnInit } from 'angular2/core';
import { IProduct } from './product';
import { ProductFilterPipe } from './product-filter.pipe';
import { StarComponent } from '../shared/star.component';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html',
    styleUrls: ['app/products/product-list.component.css'],
    pipes: [ProductFilterPipe],
    directives: [StarComponent]
})
در اینجا دو کار جدید انجام شده‌است. ابتدا خاصیت directives، به نام کلاس این کامپوننت جدید (StarComponent)، تنظیم شده‌است. سپس تعریف import ماژول این کامپوننت، به ابتدای فایل جاری اضافه شده‌است.

نمونه‌ای از اجرای برنامه را در تصویر ذیل مشاهده می‌کنید:

در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستاره‌ای این امتیازها جایگزین شده‌است و همچنین با کلیک بر روی یکی از آن‌ها، عنوان panel جاری تغییر کرده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part6.zip



خلاصه‌ی بحث

در اینجا نحوه‌ی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف می‌شوند، میدان دید آن‌ها محدود است به قالب تعریف شده‌ی متناظر با آن‌ها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرنده‌ی دیگری در دسترس قرار گیرد، آن‌را با مزین کننده‌ی ()Input@ مشخص می‌کنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، این‌کار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام می‌دهد. نوع آرگومان جنریک EventEmitter، تعیین کننده‌ی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده می‌شود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال می‌گردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده می‌شود. در اینجا هر رخدادی که توسط مزین کننده‌ی ()Output@ تعریف شده باشد، می‌تواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، می‌توان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.