معماری لایه بندی نرم افزار #2
در همین سایت مباحث الگوهای طراحی و Refactoring مفید هستند.
و یا الگوهای طراحی Agile رو هم در اینجا میتونید پیگیری کنید.
مروری بر کدهای کلاس SqlHelper
اهمیت code review
docker-compose چیست؟
فرض کنید برنامهی ما، از یک قسمت منطق خود برنامه و قسمت دیگر بانک اطلاعاتی آن تشکیل شدهاست. در این حالت برای توزیع آن توسط کانتینرها، نیاز به دو کانتینر مجزا خواهد بود؛ یکی برای برنامه و دیگری برای بانک اطلاعاتی:
dockerrun --name db` -d ` -p 3306:3306 ` -e MYSQL_ROOT_PASSWORD=my-secret-pw ` -v db:/var/lib/mysql` mysql dockerinspect db # extract ipaddress dockerrun --name web ` -d ` -p 8080:80 ` -e MY_DB_PORT=3306 ` -e MY_DB_HOST=? ` -v /my/php/app:/usr/share/nginx/html ` nginx
- دستور اول مطابق توضیحات قسمت قبل، یک بانک اطلاعاتی MySQL را در پس زمینه، با نام db که در آن، پورت 3306 میزبان به پورت 3306 کانتینر نگاشت شدهاست و همچنین بانک اطلاعاتی آن در یک volume نامدار به نام db با مسیر نگاشت شدهی به /var/lib/mysql/ داخل کانتینر ایجاد میشود، اجرا میکند.
- دستور دوم کار استخراج اطلاعات این کانتینر را انجام میدهد که شامل آدرس IP آن نیز میباشد. از این IP در برنامهی وب استفاده خواهیم کرد.
- دستور سوم مطابق توضیحات قسمت پنجم، یک وب سرور nginx را برای هاست یک برنامهی PHP که در آن پورت 8080 میزبان به پورت 80 کانتینر نگاشت شدهاست و همچنین فایلهای آن از مسیر /my/php/app/ میزبان به مسیر /usr/share/nginx/html/ داخل کانتینر نگاشت و تامین میشوند، اجرا میکند. در اینجا از پارامتر e برای تعریف یک سری متغیر محیطی مانند شمارهی پورت و IP کانتینر اجرا کنندهی mysql، استفاده شدهاست.
در این مثال دو کانتینر به هم وابسته را اجرا کردهایم و برای اجرای کانتینر دوم، نیاز است حداقل IP کانتینر اول را دانست و در قسمت MY_DB_HOST مقدار دهی کرد. روش دیگری نیز برای مدیریت سادهتر اجرای چندین کانتینر به هم وابسته توسط ابزاری به نام docker-compose وجود دارد. اگر از Dockerfile (که آنرا در قسمت پنجم معرفی کردیم) جهت ایجاد Imageهای سفارشی بکار میرود، فایل docker-compose.yml، کار خودکار سازی ایجاد و اجرای چندین کانتینر را انجام میدهد که با قالب YAML تعریف میشود:
version: '2' services: db: image: mysql ports: -3306:3306 environment: -MYSQL_ROOT_PASSWORD=my-secret-pw volumes: -db:/var/lib/mysql web: image: nginx ports: -8080:80 environment: -MY_DB_PORT=3306 -MY_DB_HOST=db volumes: -/my/php/app:/usr/share/nginx/html
در ابتدای این فایل، شماره نگارش قالب YAML مورد استفاده، مشخص شدهاست. در این نگارش، به کانتینرها، services گفته میشود که در اینجا دو سرویس db و web را مشاهده میکنید. در فایلهای yml، فضاهای خالی و indentations مهم هستند و بر این اساس است که کانتینرها و سپس مشخصات این کانتینرها، تمیز داده میشوند.
راه اندازی TeamCity به کمک فایل docker-compose.yml آن
در اینجا محتویات فایل docker-compose.yml مخصوص راه اندازی TeamCity را مشاهده میکنید که از سه کانتینر تشکیل شدهاست و از بانک اطلاعاتی postgres استفاده میکند:
version: '2' services: teamcity: image: sjoerdmulder/teamcity ports: - 8111:8111 teamcity-agent: image: sjoerdmulder/teamcity-agent environment: - TEAMCITY_SERVER=http://teamcity:8111 postgres: image: postgres environment: - POSTGRES_DB=teamcity
در ادامه برای کار با آن، ابتدا این محتویات را به صورت یک فایل متنی docker-compose.yml ذخیره کنید. سپس از طریق خط فرمان به پوشهی آن وارد شده و دستور docker-compose up را صادر کنید. docker-compose یکی دیگر از ابزارهای خط فرمان نصب شدهی به همراه داکر است و پارامتر up آن کار راه اندازی و اجرای کانتینرهای ذکر شدهی در فایل yml موجود را انجام میدهد. نام پوشهای که این فایل در آن قرار دارد، به عنوان نام پروژهی مشترک بین این کانتینرها در گزارشات آن مورد استفاده قرار میگیرد.
پس از صدور این فرمان، ابتدا تمام imageهای ذکر شدهی در فایل yml دریافت میشوند (سه image در اینجا) و هر سه کانتینر راه اندازی میشوند. اکنون میتوان در سیستم میزبان به آدرس http://localhost:8111 مراجعه کرد و از برنامهی teamcity استفاده نمود. البته صفحهی ابتدایی آن کار تنظیمات بانک اطلاعاتی آنرا انجام میدهد و جائیکه در مورد database type سؤال میپرسد میتوان postgres را انتخاب کرد و سپس در ذیل آن مقدار database host را نیز postgres وارد میکنیم. علت آنرا نیز پیشتر توضیح دادیم. postgres در اینجا نام کانتینر نیز هست و ذکر نام آن، با ذکر IP مرتبط با آن کانتینر، یکی است. نام بانک اطلاعاتی را teamcity وارد کنید (مطابق تنظیمات فایل yml فوق) و نام کاربری آن نیز postgres است؛ بدون کلمهی عبور. البته میشد در فایل yml فوق، متغیر محیطی POSTGRES_PASSWORD=xyz را نیز تنظیم کرد و سپس از آن در اینجا استفاده نمود.
docker-compose و ایجاد شبکههای ایزوله
توسط دستور docker network ls میتوان لیست شبکههای مجازی ایجاد شدهی توسط docker را مشاهده کرد (و همچنین سایر network adapters موجود). اگر این دستور را اجرا کنید، کارت شبکهی مجازی متناظر با شبکهی خصوصی teamcity_default را که پیشتر در مورد آن توضیح داده شد، میتوانید مشاهده کنید. این teamcity در اینجا همان نام پروژه و یا در اصل نام پوشهای است که فایل docker-compose را از آنجا اجرا کردیم.
برای دریافت اطلاعات بیشتری در مورد این کارت شبکهی به خصوص، میتوان دستور docker network inspect teamcity_default را صادر کرد. یکی از قسمتهای خروجی این گزارش، لیست کانتینرهایی است که هم اکنون به این شبکه متصل هستند؛ که در اینجا teamcity و بانک اطلاعاتی آن است.
مزیت ایجاد یک شبکهی خصوصی مخصوص کانتینرهای به هم پیوسته، علاوه بر سادگی تشکیل فایل docker-compose آنها با اشارهی به نام کانتینرها، بجای ذکر مستقیم آدرس IP هر کدام، ایزوله ساختن این شبکه، از شبکهی پیشفرض docker و بالا بردن میزان امنیت سایر کانتینرهایی است که هم اکنون از آن شبکه استفاده میکنند.
docker-compose و ایجاد DNS Server توکار
همانطور که عنوان شد، در این شبکهی خصوصی ویژهی کانتینرهای به هم پیوسته که توسط docker-compse اجرا و مدیریت شدهاند، میتوان از نام containerها بجای آدرس IP آنها استفاده کرد و این مورد با وجود یک DNS Server توکار در این شبکه میسر شدهاست. برای آزمایش بیشتر این قابلیت، ابتدا دستور docker ps را صادر میکنیم تا نام کانتینرهای در حال اجرا را بدست بیاوریم. سپس سعی میکنیم پروسهی bash shell داخل کانتینر بانک اطلاعاتی را اجرا کنیم:
docker ps docker exec -it teamcity_postgres_1 bash #exit
docker-compose exec postgres bash
پس از دسترسی به شل، دستور زیر را اجرا کنید:
#ping teamcity #exit
یک نکته: اگر بخواهیم از وضعیت بانکهای اطلاعاتی postgres توسط برنامهی psql آن گزارش بگیریم نیز روش اجرای آن به همین صورت است:
docker-compose exec postgres psql -U postgres postgres=#\l postgres=#\q
اتصال یک کانتینر خارج از شبکهی مجازی ایجاد شدهی توسط docker-compose به آن
فرض کنید میخواهید کانتینر کم حجم لینوکس alpine را اجرا کنید و توسط آن به شبکهی مجازی ایجاد شدهی توسط docker-compose متصل شوید. روش آن به صورت زیر است:
docker run --name apline -it --rm --net teamcity_default alpine sh
این دستور، کانتینر لینوکس alpine را در حالت interactive جهت اجرای shell آن، راه اندازی میکند. سپس به شبکهی مجازی teamcity_default متصل میشود. برای آزمایش این اتصال، در این shell راه اندازی شده، دستور ping teamcity را میتوان صادر کرد. همچنین از داخل کانتینر teamcity نیز میتوان این کانتینر را با نام آن ping کرد.
راه اندازی مجدد کانتینرها توسط docker-compose
اگر دستور docker-compose ps را دقیقا در پوشهای که فایل yml آن قرار دارد اجرا کنیم، میتوان گزارشی را صرفا از وضعیت کانتینرهای مرتبط با این فایل yml بدست آورد. دستور docker ps، لیست وضعیت تمام کانتینرهای در حال اجرای موجود را بر میگرداند. اکنون فرض کنید یکی از کانتینرهای اجرای شدهی توسط docker-compose، دیگر در حال اجرا نیست. برای راه اندازی مجدد آن میتوان از دستور docker-compose start teamcity-agent استفاده کرد. همچنین دستور docker-compose logs teamcity-agent، لیست آخرین لاگهای مرتبط با یک کانتینر را بر میگرداند که میتواند برای رفع اشکال بسیار مفید باشد.
حذف کانتینرهای به هم پیوستهی ایجاد شدهی توسط docker-compose
در ذیل ابتدا یک سری دستور را جهت مشاهدهی وضعیت سیستم مشاهده میکنید. سپس دستور docker-compose stop، کار متوقف کردن کانتینرهای مرتبط با فایل yml آنرا انجام میدهد. دستور docker-compose rm -v، علاوه بر حذف این کانتینرها، volumeهای بانکهای اطلاعاتی مرتبط را نیز حذف میکند. در آخر دستور docker-compose down، شبکهی مجازی مرتبط را نیز حذف خواهد کرد. سپس مجددا از وضعیت سیستم گزارش گیری شدهاست.
docker ps docker-compose ps docker volume ls docker network ls docker-compose stop docker-compose rm -v docker-compose down docker ps -a docker volume ls docker network ls
اجرای پروژهی ASP.NET Core Music Store توسط docker-compose
پروژهی معروف Music Store مایکروسافت را به همراه فایل docker-compose.windows.yml آن، در اینجا میتوانید مشاهده کنید. محتوای این فایل نیز به صورت زیر است:
version: '3' services: db: image: microsoft/mssql-server-windows-developer environment: sa_password: "Password1" ACCEPT_EULA: "Y" ports: - "1433:1433" # REMARK: This is currently required, needs investigation healthcheck: test: [ "CMD", "sqlcmd", "-U", "sa", "-P", "Password1", "-Q", "select 1" ] interval: 1s retries: 30 web: build: context: . dockerfile: Dockerfile.windows environment: - "Data:DefaultConnection:ConnectionString=Server=db,1433;Database=MusicStore;User Id=sa;Password=Password1;MultipleActiveResultSets=True" depends_on: - db ports: - "5000:5000"
- کانتینر db که بر اساس image مخصوص mssql-server-windows-developer راه اندازی میشود. تنظیمات آن نیز بسیار شبیه به مطلب «کار با Docker بر روی ویندوز - قسمت ششم - کار با بانکهای اطلاعاتی درون Containerها» است که پیشتر در مورد آن بحث کردیم.
- کانتینر web آن که از فایل Dockerfile.windows برای build سپس publish و در آخر run خودکار این برنامهی ASP.NET Core، کمک میگیرد. در اینجا context به پوشهی جاری اشاره میکند. در قسمت تنظیمات بانک اطلاعاتی آن، استفادهی از نام کانتینر db را در قسمت رشتهی اتصالی مشاهده میکنید. قسمت depends_on آن ترتیب اجرای این کانتینرها را مشخص میکند. یعنی ابتدا باید کانتینر db اجرا شود و سپس web.
محتوای فایل Dockerfile.windows آن نیز به صورت زیر است که بر اساس دستورات NET Core CLI. تهیه شدهاست:
FROM microsoft/dotnet-nightly:2.0-sdk-nanoserver SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] ENV NUGET_XMLDOC_MODE skip ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE 1 RUN New-Item -Path \MusicStore\samples\MusicStore -Type Directory WORKDIR app ADD samples/MusicStore/MusicStore.csproj samples/MusicStore/MusicStore.csproj ADD build/dependencies.props build/dependencies.props ADD NuGet.config . RUN dotnet restore --runtime win10-x64 .\samples\MusicStore ADD samples samples RUN dotnet publish --output /out --configuration Release --framework netcoreapp2.0 --runtime win10-x64 .\samples\MusicStore FROM microsoft/dotnet-nightly:2.0-runtime-nanoserver WORKDIR /app COPY --from=0 /out . EXPOSE 5000 ENV ASPNETCORE_URLS http://0.0.0.0:5000 CMD dotnet musicstore.dll
docker-compose -f .\docker-compose.windows.yml build docker-compose -f .\docker-compose.windows.yml up
منوهای پایین افتادنی (Dropdown menus) در Twitter Bootstrap
برای کار با منوها، حداقل نیازهایی که باید به صفحه اضافه شوند، فایل css مرتبط با Twitter Bootstrap، فایل اسکریپتهای جیکوئری و فایل bootstrap.min.js است.
در ادامه، ساختار متداول یک منوی نمونه ایجاد شده را ملاحظه میکنید:
<div class="container-fluid"> <div class="row-fluid"> <div class="span12"> <div class="dropdown"> <a class="dropdown-toggle" data-toggle="dropdown" href="#">منوی پایین افتادنی <span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> <li><a href="http://www.site3.com">site3</a></li> <li class="divider"></li> <li class="dropdown-submenu"><a href="#">سایر موارد</a> <ul class="dropdown-menu"> <li><a href="http://www.site4.com">site4</a></li> <li><a href="http://www.site5.com">site5</a></li> </ul> </li> </ul> </div> </div> </div> </div>
در اینجا به یک div، کلاس dropdown نسبت داده شده است؛ از این جهت که میتوان این منو را به انواع و اقسام المانهای صفحه نیز نسبت داد. به این ترتیب، منو از محل قرارگیری این المان باز خواهد شد. در ادامه روال متداول تعریف منو را در Bootstrap، ملاحظه میکنید. کلاس dropdown-toggle سبب خواهد شد تا با کلیک ماوس بر روی لینک، منو ظاهر شود. کلاس caret نیز سبب نمایش مثلث رو به پایین، جهت مشخص سازی وجود منو در این محل خاص، تعریف شده است.
المانهای منو، توسط لیستها تعریف میشوند. این لیست باید با کلاس dropdown-menu شروع شود. سپس هر عضو این لیست، یک آیتم منو را تشکیل خوهد داد. اگر نیاز به خط جدا کنندهای در این میان باشد، باید از کلاس divider استفاده کرد.
در اینجا همچنین امکان تعریف منوهای تو در تو نیز وجود دارد. برای اینکار تنها کافی است یک لیست دیگر را در این میان اضافه کرد. کلاس dropdown-submenu به عضو لیست دربرگیرنده زیر منو، اضافه میشود. زیر منو نیز دارای کلاس dropdown-menu خواهد بود.
چند نکته تکمیلی در مورد منوها:
- همانطور که در قسمت بررسی دکمههای Bootstrap نیز عنوان شد، برای تبدیل یک المان به دکمه فقط کافی است کلاس btn را به آن انتساب دهیم. در اینجا نیز میتوان کلاس لینک منوی پایین افتادنی را به صورت btn btn-primary dropdown-toggle تعریف کرد تا ظاهر آن تبدیل به یک دکمه Bootstrap شود.
- در اولین div مثال فوق، کلاس dropdown ذکر شده است. میتوان این کلاس را به dropup تغییر داد تا نحوه باز شدن منوی تعریف شده رو به بالا باشد؛ بجای رو به پایین.
- برای ساخت split button که تشکیل شده است از دو دکمه اصلی و دکمه کوچکی در کنار آن که یک زیرمنو را نمایش میدهد، باید به نحو زیر عمل کرد:
<div class="container-fluid"> <div class="row-fluid"> <div class="span12"> <div class="btn-group"> <a class="btn btn-primary" href="#">Split button</a> <a class="btn btn-primary dropdown-toggle" data-toggle="dropdown" href="#"><span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </div> </div> </div> </div>
تعریف برگهها در Twitter Bootstrap
برای تعریف منوهای راهبری سایت به شکل برگه، کار با استفاده از کلاس nav و تعریف یک لیست ساده شروع میشود:
<div class="container"> <div class="row"> <h2> Navigational menus </h2> <div class="span3"> <ul class="nav nav-tabs"> <li class="nav-header">nav nav-tabs</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-tabs nav-stacked"> <li class="nav-header">nav nav-tabs nav-stacked</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-pills"> <li class="nav-header">nav nav-pills</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> </ul> </div> <div class="span3"> <ul class="nav nav-list"> <li class="nav-header">nav nav-list</li> <li class="active"><a href="#">Home</a></li> <li><a href="#">Contact</a></li> <li><a href="#">Login</a></li> <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Menu ...<span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </li> </ul> </div> </div> </div>
- اضافه شدن کلاس nav، بولتهای لیست را حذف و ویژگی تغییر رنگ زمینه هر یک از عناصر لیست را با حرکت اشارهگر ماوس بر روی آن فعال میکند.
- برای اعمال اشکال مختلف به کلاس nav، میتوان از سایر کلاسهای مرتبط استفاده کرد. اگر از nav-tabs استفاده شود، این لیست عمودی تبدیل به یک لیست افقی خواهد شد. اگر کلاس active به یکی از آیتمهای آن نسبت داده شود (بیانگر صفحه جاری)، این گزینه حالت انتخاب شده را پیدا میکند.
- در هر حالت، برای تبدیل چیدمان افقی به عمودی، از کلاس nav-stacked استفاده نمائید.
- کلاس nav-pills، برای نمایش عنصر فعال، از یک مستطیل با گوشههای گرد استفاده میکند.
- اگر میخواهید این گوشهها گرد نباشند، از nav-list استفاده کنید.
- در هر کدام از حالتها میتوان از یک گزینه اختیاری با کلاس nav-header برای نمایش متنی خاکستری بالای منوی راهبری استفاده کرد.
- همانطور که در آخرین لیست اضافه شده در مثال فوق ملاحظه میکنید، امکان تعریف منوهای پایین افتادنی در اینجا نیز میسر است. در این حالت کلاس li تعریف شده باید dropdown تعریف شود. مابقی نکات آن مانند قبل است.
تعریف یک Tab به همراه محتوای برگههای آن در داخل یک صفحه
در حالت تعریف منوهای راهبری فوق، هر عنصر برگههای تعریف شده به یک صفحه جدید لینک شده بود. اگر نیاز باشد تا هر برگه، محتوایی داخل همان صفحه داشته باشد باید به نحو ذیل عمل کرد:
<div class="container"> <div class="row"> <div class="span12"> <div class="tabbable"> <ul class="nav nav-tabs"> <li class="active"><a data-toggle="tab" href="#tab1">Home</a></li> <li><a data-toggle="tab" href="#tab2">About</a></li> </ul> <div class="tab-content"> <div class="tab-pane active" id="tab1"> data ... data ... </div> <div class="tab-pane" id="tab2"> data2 ... data2 ... </div> </div> </div> </div> </div> </div>
در اینجا کار با کلاس tabbable شروع میشود. سپس از یک لیست با کلاس nav nav-tabs برای تعریف سرتیترهای برگهها استفاده میکنیم.
اگر به مثال فوق دقت کنید، اینبار hrefهای تعریف شده به tab1 و tab2 اشاره میکنند. ایندو در حقیقت id محتوای متناظر هستند که در قسمت div ایی با کلاس tab-content به div هایی با id مساوی tab1 و tab2 انتساب داده شدهاند.
شیوه دوم تعریف منوی راهبری سایت به کمک Twitter Bootstrap
اگر علاقمند باشید که منوی راهبری بالای سایت دارای یک حاشیه به همراه مسایل طراحی واکنشگرا باشد، میتوان از کامپوننت navbar مجموعه Bootstrap استفاده کرد:
<div class="container"> <div class="row"> <div class="span12"> <nav class="navbar navbar-inverse"> <div class="navbar-inner"> <a href="/" class="brand">نام سایت در اینجا</a> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </a> <div class="nav-collapse collapse"> <ul class="nav"> <li><a href="/about">درباره</a></li> <li><a href="/contact">تماس</a></li> <li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="#">Menu ...<span class="caret"></span></a> <ul class="dropdown-menu"> <li><a href="http://www.site1.com">site1</a></li> <li><a href="http://www.site2.com">site2</a></li> </ul> </li> </ul> <form class="navbar-search pull-right input-append" action="/search"> <input class="span2" type="search" /> <button class="btn" type="button"> جستجو</button> </form> </div> </div> </nav> </div> </div> </div>
navbar نیز همانند بسیاری از کامپوننتهای Bootstrap، بر اساس ul و liها کار میکند. در ابتدای کار، کلاس nav را به تگ ul انتساب میدهیم. این مورد با مباحث قسمت قبلی در مورد تدارک برگهها، تفاوتی نمیکند. به این ترتیب بولتهای لیست حذف شده و همچنین تغییر رنگ پس زمینه، با حرکت اشارهگر ماوس بر روی آیتمها را سبب میشود. سپس کل این ul و li ها، داخل تگ nav قرار میگیرند با کلاسی مساوی navbar. تا اینجا لیست عمودی تبدیل به یک لیست افقی میشود.
در ادامه برای اضافه کردن حاشیهای به اطراف این منوی راهبری و تمایز آن از سایر قسمتهای صفحه، یک div با کلاس navbar-inner را اضافه خواهیم کرد.
عموما مرسوم است که در منوی راهبری اصلی سایت، نام سایت یا متنی مشخص نیز در قسمتی از این منو ظاهر شود. برای انجام اینکار، کافی است یک لینک را با کلاس brand اضافه کنیم.
این موارد، اصول کلی تهیه یک منوی راهبری اصلی سایت را توسط Bootstrap بیان میکنند. در ادامه به چند نکته تکمیلی خواهیم پرداخت:
- اگر به تگ nav، کلاس navbar-static-top را اضافه کنیم، فاصله بین منو و اطراف صفحه را حذف میکند.
- اگر علاقمند هستیم که منوی بالای صفحه حتی با اسکرول به پایین محتوای نمایش داده شده، همواره نمایان باشد و بر روی محتوا قرار گیرد، میتوان به تگ nav، کلاس navbar-fixed-top را اضافه کرد. یا اگر نیاز است این منو در پایین صفحه به صورت ثابت نمایش داده شود از navbar-fixed-bottom میشود استفاده کرد.
در این حالت خاص ممکن است منو، قسمتی از ابتدای محتوای صفحه را مخفی کند و این محتوا زیر منو واقع شود. برای بهبود آن باید margin-top مرتبط با body را مقدار دهی کنید (حدود 40px کافی است).
- استفاده از کلاس navbar-inverse (همانند مثال فوق)، سبب معکوس شدن رنگ زمینه منوی راهبری میشود.
- همچنین در اینجا نیز میتوان یک dropdown-menu را به نحوی که ملاحظه میکنید، به منوی اصلی سایت افزود.
- امکان قرار دادن فرمهای خاصی مانند فرم جستجو نیز در منوی راهبری وجود دارد. در این حالت از کلاس navbar-search مانند مثال فوق استفاده خواهد شد. کلاس pull-right این المان را به سمت راست صفحه (در اینجا به سمت چپ با توجه به RTL بودن قالب)، هدایت میکند و کلاس input-append سبب میشود تا دکمه و تکست باکس تعریف شده در یک سطر و کنار هم قرارگیرند تا ظاهر بهتری حاصل شود. در حالت کلی کلاس navbar-form نیز برای این نوع فرمها درنظر گرفته شده است.
- اگر دقت کرده باشید، کل محتوای منو، به همراه فرم تعریف شده، در یک div با کلاس nav-collapse قرار گرفته است. هدف از اینکار ناپدید شدن خودکار قسمتهای طولانی یا نچندان مهم، در حین مرور سایت با اندازههای مرورگر کوچکتر مانند مرورگرهای موبایل است. همچنین اگر بخواهیم در این حالت که قسمتهایی ناپدید شدهاند، یک دکمه با سه سطر که بیانگر منویی مخفی است را نمایش دهیم میتوان از کلاس btn-navbar استفاده کرد؛ که نمونهای از آنرا در مثال فوق ملاحظه میکنید. این دکمه بر اساس data-target مشخص شده، سبب مخفی یا ظاهر شدن قسمتی از صفحه میگردد.
تعیین موقعیت کاربر در صفحه به کمک breadcrumbs در Bootstrap
<div class="container"> <div class="row"> <div class="span12"> <ul class="breadcrumb"> <li><a href="/level1">سطح یک</a> <span class="divider">/</span></li> <li><a href="/level2">سطح دو</a> <span class="divider">/</span></li> <li class="active">سطح سه</li> </ul> </div> </div> </div>
- در اینجا نیز برای تعریف breadcrumbs از ul و li استفاده شده است به همراه کلاس breadcrumb.
- برای ایجاد فاصله و نمایش یک خط جداکننده، از کلاس divider کمک گرفته شده است.
- صفحه جاری با کلاس active مشخص گردیده؛ بدون تعریف لینک.
تعریف قسمت صفحه بندی یک لیست یا گرید به کمک Bootstrap
<div class="container"> <div class="row"> <div class="span12"> <div class="pagination pagination-centered pagination-large"> <ul> <li class="disabled"><a href="#">Prev</a></li> <li class="active"><a href="/page/1">1</a></li> <li><a href="/page/2">2</a></li> <li><a href="/page/3">3</a></li> <li><a href="/page/4">4</a></li> <li><a href="#">Next</a></li> </ul> </div> </div> </div> </div>
برای نمایش زیباتر قسمت صفحه بندی یک لیست یا گرید میتوان از یک سری ul و li مزین شده با کلاس pagination استفاده کرد. کلاس pagination-centered، آنرا در وسط صفحه قرار خواهد داد (حالتهای چپ و راست نیز وجود دارند). کلاس pagination-large، این قسمت را اندکی بزرگتر از معمول نمایش میدهد.
در اینجا کلاس disabled، سبب غیرفعال نمایش داده شدن لینک به یک صفحه و کلاس active، بیانگر نمایش صفحه جاری است.
نمایش برچسبها و اعلانات با زمینههای مختلف به کمک Bootstrap
برای نمایش برچسبهای کوتاه با زمینهای متفاوت از کلاس label استفاده میشود. افزودن کلاسهایی مانند label-success و امثال آن که در مثال ذیل ذکر شدهاند، رنگهای متفاوتی را سبب خواهند شد. برای گرد کردن بیش از حد گوشه این برچسبها از کلاس badge استفاده میشود.
<div class="container"> <div class="row"> <div class="span12"> <section> <ul class="unstyled"> <li><span class="label">پیش فرض</span></li> <li><span class="label label-success">موفقیت آمیز</span></li> <li><span class="label label-warning">اخطار</span></li> <li><span class="label label-importatnt">مهم است</span></li> <li><span class="label label-info">اطلاعات</span></li> <li><span class="label label-inverse">رنگ معکوس</span></li> </ul> </section> <section> <ul class="unstyled"> <li><span class="badge">پیش فرض</span></li> <li><span class="badge badge-success">موفقیت آمیز</span></li> <li><span class="badge badge-warning">اخطار</span></li> <li><span class="badge badge-importatnt">مهم است</span></li> <li><span class="badge badge-info">اطلاعات</span></li> <li><span class="badge badge-inverse">رنگ معکوس</span></li> </ul> </section> <div> <div class="alert"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-danger"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-info"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-success"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> <div class="alert alert-error"> <button class="close" data-dismiss="alert">×</button> نمایش اعلانات </div> </div> </div> </div> </div>
همانطور که ملاحظه میکنید برای نمایش اعلانی به کاربر میتوان از کلاس alert استفاده کرد. افزودن کلاسهایی مانند alert-danger، رنگهای متفاوتی را ارائه خواهند داد و اگر علاقمندید که کاربر بتواند این اخطار را با کلیک بر روی دکمه ضربدر کنار آن حذف کند، از یک دکمه با کلاس close و data-dismiss مساوی alert استفاده کنید. این اطلاعات به صورت خودکار توسط اسکریپتهای bootstrap پردازش میشوند.
IFoo<string> s = ...; IFoo<object> b = s;
class Animal {} class Bear : Animal {} class Camel : Animal {} public class Stack<T> { int position; T[] data = new T[100]; public void Push (T obj) => data[position++] = obj; public T Pop() => data[--position]; }
Stack<Bear> bears = new Stack<Bear>(); Stack<Animal> animals = bears; // Compile-time error
animals.Push (new Camel()); // Trying to add Camel to bears
public class ZooCleaner { public static void Wash (Stack<Animal> animals) {...} }
class ZooCleaner { public static void Wash<T> (Stack<T> animals) where T : Animal { ... } }
Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash(bears);
Bear[] bears = new Bear[3]; Animal[] animals = bears; // OK
animals[0] = new Camel(); // Runtime error
public interface IPoppable<out T> { T Pop(); }
var bears = new Stack<Bear>(); bears.Push (new Bear()); // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>: IPoppable<Animal> animals = bears; // Legal Animal a = animals.Pop();
public class ZooCleaner { public static void Wash (IPoppable<Animal> animals) { ... } //cast covariantly to solve the reusability problem }
public interface IPushable<in T> { void Push (T obj); }
IPushable<Animal> animals = new Stack<Animal>(); IPushable<Bear> bears = animals; // Legal bears.Push (new Bear());
public interface IComparer<in T> { // Returns a value indicating the relative ordering of a and b int Compare (T a, T b); }
var objectComparer = Comparer<object>.Default; // objectComparer implements IComparer<object> IComparer<string> stringComparer = objectComparer; int result = stringComparer.Compare ("Hashem", "hashem");
برای مطالعهی بیشتر
Covariant and Contravariant
قابلیت جالبی از SQL Server 2005 به بعد به این محصول اضافه شده است که امکان ایجاد یک وب سرویس بومی را بر اساس رویههای ذخیره شده و یا توابع تعریف شده در دیتابیسهای موجود، فراهم میسازد. این قابلیت نیازی به IIS یا هر هاست دیگری برای اجرا ندارد و توسط خود اس کیوال سرور راه اندازی و مدیریت میشود.
توضیحات مفصل آنرا در MSDN میتوانید ملاحظه کنید و در اینجا یک مثال عملی از آن را با هم مرور خواهیم کرد:
الف) ایجاد یک جدول آزمایشی به همراه تعدادی رکورد دلخواه در آن
CREATE TABLE [tblWSTest](
[id] [int] IDENTITY(1,1) NOT NULL,
[f1] [nvarchar](50) NULL,
[f2] [nvarchar](500) NULL,
CONSTRAINT [PK_tblWSTest] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
SET IDENTITY_INSERT [tblWSTest] ON
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (1, N'a1', N'a2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (2, N'b1', N'b2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (3, N'c1', N'c2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (4, N'd1', N'd2')
INSERT [tblWSTest] ([id], [f1], [f2]) VALUES (5, N'e1', N'e2')
SET IDENTITY_INSERT [dbo].[tblWSTest] OFF
CREATE PROCEDURE GetAllData
AS
SELECT f1,
f2
FROM tblWSTest
CREATE ENDPOINT GetDataService
STATE = STARTED
AS HTTP(
PATH = '/GetData',
AUTHENTICATION = (INTEGRATED),
PORTS = (CLEAR),
CLEAR_PORT = 8080,
SITE = '*'
)
FOR SOAP(
WEBMETHOD 'GetAllData'
(NAME = 'testdb2009.dbo.GetAllData'),
WSDL = DEFAULT,
DATABASE = 'testdb2009',
NAMESPACE = DEFAULT
)
توضیحات:
Ports در حالت clear و یا ssl میتواند باشد. همچنین برای اینکه با IIS موجود بر روی سیستم هم تداخل نکند CLEAR_PORT به 8080 تنظیم شده است. سایر پارامترهای آن بسیار واضح هستند. برای مثال تعیین دیتابیسی که این رویه ذخیره شده در آن قرار دارد و همچنین مسیر کامل دسترسی به آن دقیقا مشخص میگردند.
این وب سرویس هم اکنون آغاز به کار کرده است. برای مشاهده wsdl آن، آدرس زیر را در مرورگر وب خود وارد نمائید (PATH و CLEAR_PORT معرفی شده در endPoint اینجا بکار میرود):
د) استفاده از این وب سرویس در یک برنامه ویندوزی
یک برنامه ساده winForms را شروع کنید. سپس یک DataGridView را بر روی فرم قرار دهید (بدیهی است این مورد میتواند یک برنامه ASP.Net هم باشد و موارد مشابه دیگر). سپس از منوی پروژه، یک service reference را در VS2008 بر اساس آدرس wdsl فوق اضافه کنید (شکل زیر):
برای اینکه این مثال در VS2008 درست کار کند باید فایل app.config ایجاد شده را کمی ویرایش کرد. قسمت security آن را یافته و تغییرات زیر را با توجه به AUTHENTICATION مورد نیاز تغییر دهید:
<security mode="TransportCredentialOnly">
<transport clientCredentialType="Windows" proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName" algorithmSuite="Default" />
</security>
using System;
using System.Data;
using System.Windows.Forms;
namespace WebServiceTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
ServiceReference1.GetDataServiceSoapClient data =
new ServiceReference1.GetDataServiceSoapClient();
dataGridView1.DataSource = (data.GetAllData()[0] as DataSet).Tables[0];
}
}
}
برای استفاده از jQuery Ajax یکی از روشهای ارسال دیتا به برنامه، تبدیل دادهها به فرمت JSON میباشد. برای دادههای ساده، تشخیص این فرمت ساده است. مثلا اگر امضای تابع وب سرویس اجکس ما به صورت زیر باشد:
public static bool IsUserAvailable(string username)
{'username':'value'}
برای این منظور در دات نت 3 و نیم، کلاسی جهت انجام اینگونه تبدیلات پیش بینی شده است که شرح مختصر آن به صورت زیر است:
ابتدا باید ارجاعی را به اسمبلی system.web.extensions به برنامه افزود و سپس جهت سهولت کار میتوان یک extension method از کلاس JavaScriptSerializer مهیا در فضای نام System.Web.Script.Serialization ایجاد کرد:
public static string ToJson(this object data)
{
return new JavaScriptSerializer().Serialize(data);
}
public static string GetJsonTest0()
{
var data = "a1";
return data.ToJson();
}
public static string GetJsonTest1()
{
var data = new List<string> { "a1", "a2", "a3" };
return data.ToJson();
}
public static string GetJsonTest2()
{
var lst =
new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("Name", "علی"),
new KeyValuePair<string, object>("Number", 10),
new KeyValuePair<string, object>("Desc", "منابع مورد نیاز")
};
return lst.ToJson();
}
"a1"
["a1","a2","a3"]
[{"Key":"Name","Value":"علی"},{"Key":"Number","Value":10},{"Key":"Desc","Value":"منابع مورد نیاز"}]
این کلاس همچنین قابلیت Deserialize و تبدیل دادههایی به فرمت JSON به اشیاء مورد نظر ما را نیز دارا است.
بعد از آمدن نسخهی سوم ASP.NET MVC مکانیسمی به نام Remote Validation به آن اضافه شد که کارش اعتبارسنجی از راه دور بود. فرض کنید نیاز است در یک فرم، قبل از اینکه کل فرم به سمت سرور ارسال شود، مقداری بررسی شده و اعتبارسنجی آن انجام گیرد و این اعتبارسنجی چیزی نیست که بتوان سمت کاربر و بدون فرستاده شدن مقداری به سمت سرور صورت گیرد. نمونه بارز این مسئله صفحه عضویت اکثر سایتهایی هست که روزانه داریم با آنها کار میکنیم. فیلد نام کاربری توسط شما پر شده و بعد از بیرون آمدن از آن فیلد، سریعا مشخص میشود که آیا این نام کاربری قابل استفاده برای شما هست یا خیر. بهصورت معمول برای انجام این کار باید با جاوا اسکریپت، مدیریتی روی فیلد مربوطه انجام دهیم. مثلا با بیرون آمدن فوکوس از روی فیلد، با Ajax نام کاربری وارد شده را به سمت سرور بفرستیم، چک کنیم و بعد از اینکه جواب برگشت بررسی کنیم که الان آیا این نام کاربری قبلا گرفته شده یا نه.
انجام این کار بهراحتی با مزینکردن خصوصیت (Property) مربوطه موجود در مدل برنامه به Attribute یا ویژگی Remote و داشتن یک Action در Controller مربوطه که کارش بررسی وجود یوزرنیم هست امکان پذیر است. ادامه بحث را با مثال همراه میکنم.
به عنوان مثال در سیستمی که قرار هست محصولات ما را ثبت کند، باید بیایم و قبل از اینکه محصول جدید به ثبت برسد این عملیات چککردن را انجام دهیم تا کالای تکراری وارد سیستم نشود. شناسه اصلی که برای هر محصول وجود دارد بارکد هست و ما آن را میخواهیم مورد بررسی قرار دهیم.
مدل برنامه
public class ProductModel { public int Id { get; set; } [Display(Name = "نام کالا")] [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")] public string Name { get; set; } [Display(Name = "قیمت")] [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [DataType(DataType.Currency)] public double Price { get; set; } [Display(Name = "بارکد")] [Required(ErrorMessage = "{0} یک فیلد اجباری است و باید آن را وارد کنید.")] [StringLength(50, ErrorMessage = "طول {0} باید کمتر از {1} کاراکتر باشد.")] [Remote("IsProductExist", "Product", HttpMethod = "POST", ErrorMessage = "این بارکد از قبل در سیستم وجود دارد.")] public string Barcode { get; set; } }
همونطور که میبینید خصوصیت Barcode را مزین کردیم به ویژگی Remote. این ویژگی دارای ورودیهای خاص خودش هست. وارد کردن نام اکشن و کنترلر مربوطه برای انجام این چککردن از مهمترین قسمتهای اصلی هست. چیزهایی دیگهای هم هست که میتوانیم آنها را مقداردهی کنیم. مثل HttpMethod، ErrorMessage و یا AdditionFields. HttpMethod که همان طریقهی ارسال درخواست به سرور هست. ErrorMessage هم همان خطایی هست که در زمان رخداد قرار است نشان داده شود. AdditionFields هم خصوصیتی را مشخص میکند که ما میخوایم بههمراه فیلد مربوطه به سمت سرور بفرستیم. مثلا میتونیم بههمراه بارکد، نام کالا را هم برای بررسیهای مورد نیازمان بفرستیم.
کنترلر برنامه
[HttpPost] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public ActionResult IsProductExist(string barcode) { if (barcode == "123456789") return Json(false); // اگر محصول وجود داشت return Json(true); }
در اینجا به نمایش قسمتی از کنترلر برنامه میپردازیم. اکشنی که مربوط میشود به چککردن مقدارهای لازم و در پایان آن یک خروجی Json را برمیگردانیم که مقدار true یا false دارد. در حقیقت مقدار را به این صورت برمیگردانیم که اگر مقدار ورودی در پایگاه داده وجود دارد، false را برمیگرداند و اگر وجود نداشت true. همینطور آمدیم از کش شدن درخواستهایی که با Ajax آمده با ویژگی OutputCache جلوگیری کردیم.