[HttpGet] public ActionResult GetFile(string path) { path = GetSafeFileAndDirPath(path); return File(path, "image/png"); }
البته مثال فوق آزمایش شدهاست و در این بین از روش دیباگ اسکریپتها و یافتن خطاهای سمت سرور و کاربر کمک گرفته شد (کار کردن با کتابخانههای جاوا اسکریپتی، بدون باز نگه داشتن کنسول developer مرورگرها تقریبا غیر ممکن است).
من در پروژه اخیرم برای نمایش یک سری سوال مجبور بودم که در هر بار نمایش سوالات، لیست را به صورت رندوم مرتب کنم و به کاربر نمایش بدم. برای حصول این مهم، یک extension method به شکل زیر نوشتم:
public static class RandomExtentions { public static void Shuffle<T>(this IList<T> list) { Random rng = new Random(); Thread.Sleep(100); int n = list.Count; while (n > 1) { n--; int k = rng.Next(n + 1); T value = list[k]; list[k] = list[n]; list[n] = value; } } }
مثال :
var x =new List<int>(); x.Add(1); x.Add(2); x.Add(3); x.Add(4); x.Add(5); x.Shuffle();
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
استفادهی استاتیک از افزونه Typeahead
منظور از استفادهی استاتیک، مشخص بودن آرایه عناصر و هچنین درج آن به صورت html encoded در صفحه است. برای این منظور، کنترلر برنامه چنین شکلی را خواهد داشت:
using System.Web.Mvc; using System.Web.Script.Serialization; namespace Mvc4TwitterBootStrapTest.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { var array = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; ViewBag.JsonString = new JavaScriptSerializer().Serialize(array); return View(); } } }
View متناظر با آن به نحو ذیل با مشخص سازی نوع data-provide (تا به کتابخانهی جاوا اسکریپتی همراه bootstrap اعلام کند از چه افزونهای در اینجا قرار است استفاده شود)، منبع داده data-source و حداکثر تعداد آیتم ظاهر شونده data-items، میتواند طراحی شود:
@{ ViewBag.Title = "Index"; } <h2> Typeahead</h2> @Html.TextBox("search", null, htmlAttributes: new { autocomplete = "off", data_provide = "typeahead", data_items = 8, data_source = @ViewBag.JsonString })
<input autocomplete="off" data-items="8" data-provide="typeahead" data-source="["Afghanistan","Albania","Algeria","American Samoa","Andorra","Angola","Anguilla","Antarctica","Antigua and/or Barbuda"]" id="search" name="search" type="text" value="" />
اگر هم بخواهیم برای آن یک Html Helper درست کنیم، میتوان به نحو ذیل عمل کرد:
public static MvcHtmlString TypeaheadFor<TModel, TValue>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, IEnumerable<string> source, int items = 8) { var jsonString = new JavaScriptSerializer().Serialize(source); return htmlHelper.TextBoxFor( expression, new { autocomplete = "off", data_provide = "typeahead", data_items = items, data_source = jsonString } ); }
استفاده پویا و Ajax ایی از افزونه Typeahead
اگر بخواهیم data-source را به صورت پویا، هربار از بانک اطلاعاتی دریافت و ارائه دهیم، نیاز به کمی اسکریپت نویسی خواهد بود:
using System; using System.Linq; using System.Web.Mvc; using System.Web.Script.Serialization; namespace Mvc4TwitterBootStrapTest.Controllers { public class HomeController : Controller { [HttpGet] public JsonResult GetNames(string term) { var array = new[] { "Afghanistan", "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla", "Antarctica", "Antigua and/or Barbuda" }; var results = array.Where(n => n.StartsWith(term, StringComparison.OrdinalIgnoreCase)); return Json(results.ToArray(), JsonRequestBehavior.AllowGet); } } }
سپس در تعاریف View، قسمت data-source مرتبط با TextBox حذف و از طریق فراخوانی مستقیم کدهای افزونه typeahead مقدار دهی میگردد:
@{ ViewBag.Title = "Index"; var url = Url.Action("GetNames", "Home"); } <h2> Typeahead</h2> @Html.TextBox("search", null, htmlAttributes: new { autocomplete = "off", data_provide = "typeahead", data_items = 8 }) @section JavaScript { <script type="text/javascript"> $(function () { $('#search').typeahead({ source: function (term, process) { return $.getJSON('@url', { term: term }, function (data) { return process(data); }); } }); }); </script> }
چندی پیش در سایت جاری چند مقاله خوب توسط یکی از دوستان درباره Qunit منتشر شد. Qunit یک ابزار قدرتمند و مناسب برای تست کدهای جاوااسکریپت است و در اثبات صحت این گفته همین کافیست که بدانیم برای تست کدهای نوشته شده در پروژههای متن بازی هم چون Backbone.Js و JQuery از این فریم ورک استفاده شده است. اما به احتمال قوی در ذهن شما این سوال مطرح شده است که خب! در صورت آشنایی با Qunit چه نیاز به یادگیری Jasmine یا خدای نکرده Mocha و FuncUnit است؟ هدف صرفا معرفی یک ابزار غیر برای تست کد است نه مقایسه و نتیجه گیری برای تعیین میزان برتری این ابزارها. اصولا مهمترین دلیل برای انتخاب، علاوه بر امکانات و انعطاف پذیری، فاکتور راحتی و آسان بودن در هنگام استفاده است که به صورت مستقیم به شما و تیم توسعه نرم افزار بستگی دارد.
اما به عنوان توسعه دهنده نرم افزار که قرار است از این ابزار استفاده کنیم بهتر است با تفاوتها و شباهتهای مهم این دو فریم ورک آشنا باشیم:
»Jasmine یک فریم ورک تست کدهای جاوا اسکریپ بر مبنای Behavior-Driven Development است در حالی که Qunit بر مبنای Test-Driven Development است و همین مسئله مهمترین تفاوت بین این دو فریم ورک میباشد.
»اگر قصد دارید که از Qunit نیز به روش BDD استفاده نمایید باید از ترکیب Pavlov به همراه Qunit استفاده کنید.
»Jasmine از مباحث مربوط به Spies و Mocking به خوبی پشتیبانی میکند ولی این امکان به صورت توکار در Qunit فراهم نیست. برای اینکه بتوانیم این مفاهیم را در Qunit پیاده سازی کنیم باید از فریم ورکهای دیگر نظیر SinonJS به همراه Qunit استفاده کنیم.
»هر دو فریم ورک بالا به سادگی و راحتی کار معروف هستند
»تمام موارد مربوط به الگوهای Matching در هر دو فریم ورک به خوبی تعبیه شده است
» هر دو فریم ورک بالا از مباحث مربوط به Asynchronous Testing برای تست کدهای Ajax ای به خوبی پشتیبانی میکنند.
بررسی چند مفهوم
قبل از شروع، بهتر است که با چند مفهوم کلی و در عین حال مهم این فریم ورک آشنا شویم
describe('JavaScript addition operator', function () { it('adds two numbers together', function () { expect(1 + 2).toEqual(3); }); });
در تابع it کد بالا شما میتوانید کدهای مربوط بدنه توابع تست خود را بنویسید. برای پیاده سازی Assert در توابع تست مفهوم expectationها وجود دارد. در واقع expect برای بررسی مقادیر حقیقی با مقادیر مورد انتظار مورد استفاده قرار میگیرد و شامل مقادیر true یا false خواهد بود.
برای Setup و Teardown توابع تست خود باید از توابع beforeEach و afterEach که بدین منظور تعبیه شده اند استفاده کنید.
describe("A spec (with setup and tear-down)", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); afterEach(function() { foo = 0; }); it("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); it("can have more than one expectation", function() { expect(foo).toEqual(1); expect(true).toEqual(true); }); });
اگر در کد تست خود قصد دارید که یک تابع describe یا it را غیر فعال کنید کافیست یک x به ابتدای آنها اضافه کنید و دیگر نیاز به هیچ کار اضافه دیگری برای comment کردن کد نیست.
xdescribe("A spec", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); xit("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); });
درادامه قصد پیاده سازی یک مثال را با استفاده از Jasmine و RequireJs در پروژه Asp.Net MVC دارم.
- فولدر lib شامل فایلها کدهای Jasmine برای setup و tear down و spice و تست کدهای شما میباشد.
- فایل specRunner.html به واقع یک فایل برای نمایش فایلهای تست و همچنین نمایش نتیجه تست است.
- فولدر spec نیز شامل کدهای Jasmine برای کمک به نوشتن تست میباشد.
در این مثال قصد داریم فایلهای player.js و song.js که به عنوان نمونه به همراه این فریم ورک قرار دارد را در قالب یک پروژه MVC به همراه RequireJs، تست نماییم. در نتیجه این فایلها را از فولدر src انتخاب نمایید و آنها را در قسمت Scripts پروژه اصلی خود کپی کنید(ابتدا بک پوشه به نام App بسازید و فایلها را در آن قرار دهید)
برای استفاده از requireJs باید دستور define را در ابتدا این فایلها اضافه نماییم. در نتیجه فایلهای Player.js و Song.js را باز کنید و تغییرات زیر را در ابتدای این فایلها اعمال نمایید.
Song.js
define(function () { function Song() { } Song.prototype.persistFavoriteStatus = function (value) { // something complicated throw new Error("not yet implemented"); }; });
define(function () { function Player() { } Player.prototype.play = function (song) { this.currentlyPlayingSong = song; this.isPlaying = true; }; Player.prototype.pause = function () { this.isPlaying = false; }; Player.prototype.resume = function () { if (this.isPlaying) { throw new Error("song is already playing"); } this.isPlaying = true; }; Player.prototype.makeFavorite = function () { this.currentlyPlayingSong.persistFavoriteStatus(true); }; });
baseUrl در پیکر بندی requireJs به مسیر فایلهای پروژه که در پروژه اصلی MVC قرار دارد اشاره میکند. paths برای تعیین مسیر فایلهای تست که در پوشه spec در پروژه تست قرار دارد اشاره میکند. اگر دقت کرده باشید به دلیل اینگه تگهای script مربوط به لود فایلهای SpecHelper.js و PlayerSpec.js به صورت comment در آمده اند در نتیجه این فایلها لود نخواهند شد و خروجی مورد نظر مشاهده نمیشود. در این جا باید از مکانیزم AMD موجود در RequireJs استفاده نماییم و فایلهای مربوطه را لود کنیم. برای این کار نیاز به اضافه کردن دستور require در ابتدای تگ script به صورت زیر در این فایل است. در نتیجه فایلهای PlayerSpec و SpecHelper نیز توسط RequireJs لود خواهند شد.
نیاز به یک تغییر کوچک دیگر نیز وجود دارد. فایل PlayerSpec را باز نمایید و وابستگی فایلهای آن را تعیین نمایید. از آن جا که این فایل برای تست فایلهای Player , Song ایجاد شده است در نتیجه باید از define برای تعیین این وابستگیها استفاده نماییم.
یادآوری:
»دستور describe در فایل بالا برای تعریف تابع تست است. همان طور که میبینید بک نام به آن داده میشود به همراه بدنه تابع تست.
»دستور beforeEach برای آماده سازی مواردی است که قصد داریم در تست مورد استفاده قرار گیرند. همانند متدهای Setup در UnitTest.
» دستور expect نیز معادل Assert در UnitTest است و برای بررسی صحت عملکرد تست نوشته میشود.
اگر
فایل SpecRunner.html را دوباره در مرورگر خود باز نمایید تصویر زیر را
مشاهده خواهید کرد که به عنوان موفقیت آمیز بودن پیکر بندی پروژه و تستهای آن میباشد.
در این قسمت با نحوهی تناظر آدرسها توسط Web API به متدهای موجود در Controller آشنا میشوید.
در هر درخواستی که ارسال میشود، Web API، انتخاب Controller مناسب را با رجوع به جدولی با نام جدول مسیرها انجام میدهد. زمانی که یک پروژهی جدید با استفاده از ASP.NET MVC 4 ایجاد میکنید، یک route پیش فرض به صورت ذیل در متد RegisterRoutes قرار میگیرد.
routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
- {controller} به نام Controller تناظر پیدا میکند.
- نوع درخواست ارسالی (GET، POST، PUT، DELETE) به نام متد تناظر پیدا میکند.
- اگر قسمت {id} در آدرس وجود داشته باشد، به پارامتر id متد انتخاب شده پاس داده میشود.
- اگر آدرس دارای Query String باشد، به پارامترهای همنام خود در متد، تناظر پیدا میکنند.
در ذیل، مثال هایی را از چند آدرس درخواستی و نتیجهی حاصل از فراخوانی آنها مشاهده میکنید.
- آدرس api/products/ با نوع درخواست GET به متد ()GetAllProducts
- آدرس api/products/1/ با نوع درخواست GET به متد (1)GetProductById
- آدرس api/products?category=hardware/ با نوع درخواست GET به متد ("GetProductByCategory("hardware
در آدرس اول، عبارت "products" به ProductsController تطبیق پیدا میکند. درخواست نیز از نوع GET است، بنابراین Web API به دنبال متدی در Controller میگردد که نام آن با عبارت GET "آغاز" شده باشد. همچنین، آدرس شامل قسمت {id} نیز نیست. بنابراین، Web API متدی را انتخاب میکند که پارامتر ورودی ندارد. متد GetAllProducts در ProductsController، تمامی این شروط را دارد، پس انتخاب میشود.
در دومین آدرس، همان حالت قبل وجود دارد، با این تفاوت که در آدرس درخواستی، قسمت {id} وجود دارد. از آنجا که نوع قسمت {id} در متد int ،GetProductById تعریف شده است، باید یک عدد صحیح بعد از آدرس /api/products وجود داشته باشد تا متد GetProductById فراخوانی شود. این عدد به طور خودکار به نوع int تبدیل شده و در پارامتر اول متد GetProductById قرار میگیرد. در ذیل، برخی آدرسها را ملاحظه میکنید که معتبر نیستند و باعث بروز خطا میشوند.
- آدرس api/products/ با نوع درخواست POST، باعث خطای 405Method Not Allowed میشود.
- آدرس api/users/ با نوع درخواست GET، باعث خطای 404Not Found میشود.
- آدرس api/products/abc/ با نوع درخواست GET، باعث خطای 400Bad Request میشود.
در آدرس اول، Client یک درخواست از نوع POST ارسال کرده است. Web API به دنبال متدی میگردد که نام آن با عبارت Post آغاز میشود. اما متدی با این شرط در ProductsController وجود ندارد. بنابراین، پاسخی که دریافت میشود، عبارت "405 Method Not Allowed" است. درخواست برای آدرس /api/users/ نیز معتبر نیست، چون Controllerیی با نام UsersController وجود ندارد. و سومین آدرس نیز بدین دلیل نامعتبر است که قسمت abc نمیتواند به یک عدد صحیح تبدیل شود.
زمانی که با یک وب سرویس کار میکنید، مشاهدهی محتویات درخواست ارسالی و پاسخ دریافتی میتواند کاربرد زیادی در درک نحوهی تعامل بین Client و وب سرویس و کشف خطاهای احتمالی داشته باشد. در Firefox با استفاده از افزونهی Firebug و در Internet Explorer 9 به بالا با ابزار Developer Tools آن میتوان درخواستها و پاسخها را مشاهده کرد. در Internet Explorer، کلید F12 را برای اجرای Developer Tools فشار دهید. از قسمت Network بر روی دکمهی Start Capturing کلیک کنید. حال کلید F5 را برای بارگذاری مجدد صفحه فشار دهید. Internet Explorer، درخواست و پاسخ رد و بدل شده بین مرورگر و Web Server را مانیتور کرده و گزارشی را نشان میدهد (شکل ذیل).
از ستون URL، آدرس /api/products/ را انتخاب و بر روی دکمهی Go to detailed view کلیک کنید. در قسمتی که باز میشود، گزینه هایی برای مشاهدهی هدرهای درخواست، پاسخ و همچنین بدنهی هر یک وجود دارد. به عنوان مثال، اگر قسمت Request headers را انتخاب کنید، خواهید دید که Internet Explorer از طریق هدر Accept، تقاضای پاسخ در قالب JSON را کرده است (شکل ذیل).
اگر قسمت Response body را انتخاب کنید، پاسخ دریافت شده در قالب JSON را خواهید دید.
ASP.NET Web API - قسمت اول
از این لیست رسمی، دو مورد معروف آن در سایت جاری بررسی شده:
ASP.NET Identity
Forms authentication
مباحث پایهای اینها مشترک است بین MVC و وب فرمها و سایر فناوریهای مشابه.
Kendo UI MVVM
- «استفاده از Kendo UI templates »
- «اعتبار سنجی ورودیهای کاربر در Kendo UI»
- «فعال سازی عملیات CRUD در Kendo UI Grid» جهت آشنایی با نحوهی تعریف DataSource ایی که میتواند اطلاعات را ثبت، حذف و یا ویرایش کند.
در این مطلب قصد داریم به یک چنین صفحهای برسیم که در آن در ابتدای نمایش، لیست ثبت نامهای موجود، از سرور دریافت و توسط یک Kendo UI template نمایش داده میشود. سپس امکان ویرایش و حذف هر ردیف، وجود خواهد داشت، به همراه امکان افزودن ردیفهای جدید. در این بین مدیریت نمایش لیست ثبت نامها توسط امکانات binding توکار فریم ورک MVVM مخصوص Kendo UI صورت خواهد گرفت. همچنین کلیه اعمال مرتبط با هر ردیف نیز توسط data binding دو طرفه مدیریت خواهد شد.
Kendo UI MVVM
الگوی MVVM یا Model-View-ViewModel که برای اولین بار جهت کاربردهای WPF و Silverlight معرفی شد، برای ساده سازی اتصال تغییرات کنترلهای برنامه به خواص ViewModel یک View کاربرد دارد. برای مثال با تغییر عنصر انتخابی یک DropDownList در یک View، بلافاصله خاصیت متصل به آن که در ViewModel برنامه تعریف شدهاست، مقدار دهی و به روز خواهد شد. هدف نهایی آن نیز جدا سازی منطق کدهای UI، از کدهای جاوا اسکریپتی سمت کاربر است. برای این منظور کتابخانههایی مانند Knockout.js به صورت اختصاصی برای این کار تهیه شدهاند؛ اما Kendo UI نیز جهت یکپارچگی هرچه تمامتر اجزای آن، دارای یک فریم ورک MVVM توکار نیز میباشد. طراحی آن نیز بسیار شبیه به Knockout.js است؛ اما با سازگاری 100 درصد با کل مجموعه.
پیاده سازی الگوی MVVM از 4 قسمت تشکیل میشود:
- Model که بیانگر خواص متناظر با اشیاء رابط کاربری است.
- View همان رابط کاربری است که به کاربر نمایش داده میشود.
- ViewModel واسطی است بین Model و View. کار آن انتقال دادهها و رویدادها از View به مدل است و در حالت binding دوطرفه، عکس آن نیز صحیح میباشد.
- Declarative data binding جهت رهایی برنامه نویسها از نوشتن کدهای هماهنگ سازی اطلاعات المانهای View و خواص ViewModel کاربرد دارد.
در ادامه این اجزا را با پیاده سازی مثالی که در ابتدای بحث مطرح شد، دنبال میکنیم.
تعریف Model و ViewModel
در سمت سرور، مدل ثبت نام برنامه چنین شکلی را دارد:
namespace KendoUI07.Models { public class Registration { public int Id { set; get; } public string UserName { set; get; } public string CourseName { set; get; } public int Credit { set; get; } public string Email { set; get; } public string Tel { set; get; } } }
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ id: "Id", fields: { Id: { type: 'number' }, // leave this set to 0 or undefined, so Kendo knows it is new. UserName: { type: 'string' }, CourseName: { type: 'string' }, Credit: { type: 'number' }, Email: { type: 'string' }, Tel: { type: 'string' } } }); }); </script>
<script type="text/javascript"> $(function () { var viewModel = kendo.observable({ accepted: false, course: new model() }); }); </script>
اتصال ViewModel به View برنامه
تعریف فرم ثبت نام را در اینجا ملاحظه میکنید. فیلدهای مختلف آن بر اساس نکات اعتبارسنجی HTML 5 با ویژگیهای خاص آن، مزین شدهاند. جزئیات آنرا در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» پیشتر بررسی کردهایم.
اگر به تعریف هر فیلد دقت کنید، ویژگی data-bind جدیدی را هم ملاحظه خواهید کرد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <h3>ثبت نام</h3> <ul> <li> <label for="Id">Id</label> <span id="Id" data-bind="text:course.Id"></span> </li> <li> <label for="UserName">نام</label> <input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required /> </li> <li> <label for="CourseName">دوره</label> <input type="text" dir="ltr" id="CourseName" name="CourseName" required data-bind="value:course.CourseName" /> <span class="k-invalid-msg" data-for="CourseName"></span> </li> <li> <label for="Credit">مبلغ پرداختی</label> <input id="Credit" name="Credit" type="number" min="1000" max="6000" required data-max-msg="عددی بین 1000 و 6000" dir="ltr" data-bind="value:course.Credit" class="k-textbox k-input" /> <span class="k-invalid-msg" data-for="Credit"></span> </li> <li> <label for="Email">پست الکترونیک</label> <input type="email" id="Email" dir="ltr" name="Email" data-bind="value:course.Email" required class="k-textbox" /> </li> <li> <label for="Tel">تلفن</label> <input type="tel" id="Tel" name="Tel" dir="ltr" pattern="\d{8}" required class="k-textbox" data-bind="value:course.Tel" data-pattern-msg="8 رقم" /> </li> <li> <input type="checkbox" name="Accept" data-bind="checked:accepted" required /> شرایط دوره را قبول دارم. <span class="k-invalid-msg" data-for="Accept"></span> </li> <li> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button> </li> </ul> <span id="doneMsg"></span> </form> </div>
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ // ... }); var viewModel = kendo.observable({ // ... }); kendo.bind($("#coursesSection"), viewModel); }); </script>
<input type="text" id="UserName" name="UserName" class="k-textbox" data-bind="value:course.UserName" required />
بنابراین تا اینجا به صورت خلاصه، مدلی را توسط متد kendo.data.Model.define، معادل مدل سمت سرور خود ایجاد کردیم. سپس وهلهای از این مدل را به صورت یک خاصیت جدید دلخواهی در ViewModel تعریف شده توسط متد kendo.observable در معرض دید View برنامه قرار دادیم. در ادامه اتصال ViewModel و View، با فراخوانی متد kendo.bind انجام شد. اکنون برای دریافت تغییرات کنترلهای برنامه، تنها کافی است ویژگیهای data-bind ایی را به آنها اضافه کنیم.
در ناحیهی تعریف شده توسط متد kendo.bind، کلیه خواص ViewModel در دسترس هستند. برای مثال اگر به تعریف ViewModel دقت کنید، یک خاصیت دیگر به نام accepted با مقدار false نیز در آن تعریف شدهاست (این خاصیت چون صرفا کاربرد UI داشت، در model برنامه قرار نگرفت). از آن برای اتصال checkbox تعریف شده، به button ارسال اطلاعات، استفاده کردهایم:
<input type="checkbox" name="Accept" data-bind="checked:accepted" required /> <button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button>
ارسال دادههای تغییر کردهی ViewModel به سرور
تا اینجا 4 جزء اصلی الگوی MVVM که در ابتدای بحث عنوان شد، تکمیل شدهاند. مدل اطلاعات فرم تعریف گردید. ViewModel ایی که این خواص را به المانهای فرم متصل میکند نیز در ادامه اضافه شدهاست. توسط ویژگیهای data-bind کار Declarative data binding انجام میشود.
در ادامه نیاز است تغییرات ViewModel را به سرور، جهت ثبت، به روز رسانی و حذف نهایی منتقل کرد.
<script type="text/javascript"> $(function () { var model = kendo.data.Model.define({ //... }); var dataSource = new kendo.data.DataSource({ type: 'json', transport: { read: { url: "api/registrations", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, create: { url: "api/registrations", contentType: 'application/json; charset=utf-8', type: "POST" }, update: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "PUT" }, destroy: { url: function (course) { return "api/registrations/" + course.Id; }, contentType: 'application/json; charset=utf-8', type: "DELETE" }, parameterMap: function (data, type) { // Convert to a JSON string. Without this step your content will be form encoded. return JSON.stringify(data); } }, schema: { model: model }, error: function (e) { alert(e.errorThrown); }, change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); var viewModel = kendo.observable({ //... }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
متصل کردن DataSource به ViewModel
تا اینجا DataSource ایی جهت کار با سرور تعریف شدهاست؛ اما مشخص نیست که اگر رکوردی اضافه شد، چگونه باید اطلاعات خودش را به روز کند. برای این منظور خواهیم داشت:
<script type="text/javascript"> $(function () { $("#coursesSection").kendoValidator({ // ... }); var model = kendo.data.Model.define({ // ... }); var dataSource = new kendo.data.DataSource({ // ... }); var viewModel = kendo.observable({ accepted: false, course: new model(), doSave: function (e) { e.preventDefault(); console.log("this", this.course); var validator = $("#coursesSection").data("kendoValidator"); if (validator.validate()) { if (this.course.Id == 0) { dataSource.add(this.course); } dataSource.sync(); // push to the server this.set("course", new model()); // reset controls } }, resetModel: function (e) { e.preventDefault(); this.set("course", new model()); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
در متد doSave، ابتدا بررسی میکنیم آیا اعتبارسنجی فرم با موفقیت انجام شدهاست یا خیر. اگر بله، توسط متد add منبع داده، اطلاعات فرم جاری را توسط شیء course که هم اکنون به تمامی فیلدهای آن متصل است، اضافه میکنیم. در اینجا بررسی شدهاست که آیا Id این اطلاعات صفر است یا خیر. از آنجائیکه از همین متد برای به روز رسانی نیز در ادامه استفاده خواهد شد، در حالت به روز رسانی، Id شیء ثبت شده، از طرف سرور دریافت میگردد. بنابراین غیر صفر بودن این Id به معنای عملیات به روز رسانی است و در این حالت نیازی نیست کار بیشتری را انجام داد؛ زیرا شیء متناظر با آن پیشتر به منبع داده اضافه شدهاست.
استفاده از متد add صرفا به معنای مطلع کردن منبع داده محلی از وجود رکوردی جدید است. برای ارسال این تغییرات به سرور، از متد sync آن میتوان استفاده کرد. متد sync بر اساس متد add یک درخواست POST، بر اساس شیءایی که Id غیر صفر دارد، یک درخواست PUT و با فراخوانی متد remove بر روی منبع داده، یک درخواست DELETE را به سمت سرور ارسال میکند.
متد دلخواه resetModel سبب مقدار دهی مجدد شیء course با یک وهلهی جدید از شیء model میشود. همینقدر برای پاک کردن تمامی کنترلهای صفحه کافی است.
تا اینجا دو متد جدید را در ViewModel برنامه تعریف کردهایم. در مورد نحوهی اتصال آنها به View، به کدهای دو دکمهی موجود در فرم دقت کنید:
<button class="k-button" data-bind="enabled: accepted, click: doSave" type="submit"> ارسال </button> <button class="k-button" data-bind="click: resetModel">از نو</button>
مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات
در حالت ثبت، متد Post توسط آدرس مشخص شده در قسمت create منبع داده، فراخوانی میگردد. نکتهی مهمی که در اینجا باید به آن دقت داشت، نحوهی بازگشت Id رکورد جدید ثبت شدهاست. اگر این تنظیم صورت نگیرد، Id رکورد جدید را در لیست، مساوی صفر مشاهده خواهید کرد و منبع داده این رکورد را همواره به عنوان یک رکورد جدید، مجددا به سرور ارسال میکند.
using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using KendoUI07.Models; namespace KendoUI07.Controllers { public class RegistrationsController : ApiController { public HttpResponseMessage Delete(int id) { var item = RegistrationsDataSource.LatestRegistrations.FirstOrDefault(x => x.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); RegistrationsDataSource.LatestRegistrations.Remove(item); return Request.CreateResponse(HttpStatusCode.OK, item); } public IEnumerable<Registration> Get() { return RegistrationsDataSource.LatestRegistrations; } public HttpResponseMessage Post(Registration registration) { if (!ModelState.IsValid) return Request.CreateResponse(HttpStatusCode.BadRequest); var id = 1; var lastItem = RegistrationsDataSource.LatestRegistrations.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } registration.Id = id; RegistrationsDataSource.LatestRegistrations.Add(registration); // ارسال آی دی مهم است تا از ارسال رکوردهای تکراری جلوگیری شود return Request.CreateResponse(HttpStatusCode.Created, registration); } [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT' public HttpResponseMessage Update(int id, Registration registration) { var item = RegistrationsDataSource.LatestRegistrations .Select( (prod, index) => new { Item = prod, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null) return Request.CreateResponse(HttpStatusCode.NotFound); if (!ModelState.IsValid || id != registration.Id) return Request.CreateResponse(HttpStatusCode.BadRequest); RegistrationsDataSource.LatestRegistrations[item.Index] = registration; return Request.CreateResponse(HttpStatusCode.OK); } } }
نمایش آنی اطلاعات ثبت شده در یک لیست
ردیفهای اضافه شده به منبع داده را میتوان بلافاصله در همان سمت کلاینت توسط Kendo UI Template که قابلیت کار با ViewModelها را دارد، نمایش داد:
<div id="coursesSection" class="k-rtl k-header"> <div class="box-col"> <form id="myForm" data-role="validator" novalidate="novalidate"> <!--فرم بحث شده در ابتدای مطلب--> </form> </div> <div id="results"> <table class="metrotable"> <thead> <tr> <th>Id</th> <th>نام</th> <th>دوره</th> <th>هزینه</th> <th>ایمیل</th> <th>تلفن</th> <th></th> <th></th> </tr> </thead> <tbody data-template="row-template" data-bind="source: coursesDataSourceRows"></tbody> <tfoot data-template="footer-template" data-bind="source: this"></tfoot> </table> <script id="row-template" type="text/x-kendo-template"> <tr> <td data-bind="text: Id"></td> <td data-bind="text: UserName"></td> <td dir="ltr" data-bind="text: CourseName"></td> <td> #: kendo.toString(get("Credit"), "c0") # </td> <td data-bind="text: Email"></td> <td data-bind="text: Tel"></td> <td><button class="k-button" data-bind="click: deleteCourse">حذف</button></td> <td><button class="k-button" data-bind="click: editCourse">ویرایش</button></td> </tr> </script> <script id="footer-template" type="text/x-kendo-template"> <tr> <td colspan="3"></td> <td> جمع کل: #: kendo.toString(totalPrice(), "c0") # </td> <td colspan="2"></td> <td></td> <td></td> </tr> </script> </div> </div>
<script type="text/javascript"> $(function () { // ... var viewModel = kendo.observable({ accepted: false, course: new model(), coursesDataSourceRows: new kendo.data.ObservableArray([]), doSave: function (e) { // ... }, resetModel: function (e) { // ... }, totalPrice: function () { var sum = 0; $.each(this.get("coursesDataSourceRows"), function (index, item) { sum += item.Credit; }); return sum; }, deleteCourse: function (e) { // the current data item is passed as the "data" field of the event argument var course = e.data; dataSource.remove(course); dataSource.sync(); // push to the server }, editCourse: function(e) { // the current data item is passed as the "data" field of the event argument var course = e.data; this.set("course", course); } }); kendo.bind($("#coursesSection"), viewModel); dataSource.read(); // دریافت لیست موجود از سرور در آغاز کار }); </script>
- ابتدا خاصیت دلخواه coursesDataSourceRows به viewModel اضافه میشود تا در ناحیهی coursesSection در دسترس قرار گیرد.
- سپس اگر به انتهای تعریف DataSource دقت کنید، داریم:
<script type="text/javascript"> $(function () { var dataSource = new kendo.data.DataSource({ //... change: function (e) { // فراخوانی در زمان دریافت اطلاعات از سرور و یا تغییرات محلی viewModel.set("coursesDataSourceRows", new kendo.data.ObservableArray(this.view())); } }); }); </script>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI07.zip
به شخصه اعتقادی ندارم که جهت مدیریت کار رایگانی که انجام میشود از امکانات غیر رایگان استفاده کرد. تابحال برای ذخیره سازی کدهای منتشر شده در این وبلاگ از persiangig تا googlepages مرحوم تا رپیدشیر تا ... استفاده کردهام. نه امکان لیست کردن سریع آنها موجود است و نه مشخص است که چه تعدادی از آنها هنوز وجود خارجی داشته و از سرورهای یاد شده پاک نشدهاند. اخیرا تعدادی وبلاگ برنامه نویسی را یافتهام که از سایت CodePlex به عنوان مخزنی برای ذخیره سازی کدها و مثالهای منتشر شده در وبلاگ خود استفاده میکنند. این کار چند مزیت دارد:
- رایگان است (فضا، پهنای باند، اسکریپت و غیره)
- به صورت تضمینی تا 10 سال دیگر هم پابرجا است.
- درب آن به روی کاربران ایرانی باز است (برخلاف مثلا سایت googlecodes یا رفتار اخیر سورس فورج و غیره، سایت CodePlex در این چندسال رویه ثابتی داشته است)
- امکان مشاهدهی لیست تمامی کدهای منتشر شده موجود است.
- امکان ثبت توضیحات کنار هر کد منتشر شده نیز وجود دارد.
- امکان دریافت یکجای آنها با توجه به استفاده از ابزارهای سورس کنترل مهیا است.
- امکان دریافت بهینهی موارد جدید هم برای کاربران وجود دارد. کاربری که یکبار با استفاده از ابزارهای سورس کنترل، کدهای موجود را دریافت کرده، در بار بعدی دریافت اطلاعات، تنها موارد تغییر کرده یا جدید را دریافت خواهد کرد و نه تمام اطلاعات کل مخزن را از ابتدا تا به امروز.
- امکان مشاهدهی آمار دریافتها، مراجعات، سایتهایی که به شما لینک دادهاند و غیره فراهم است.
- امکان دعوت کردن از افراد دیگر نیز جهت به روز رسانی مخزن کد تدارک دیده شده است.
- کلیه اعضای CodePlex بدون نیاز به عضویت در گروه مخزن کد شما، میتوانند جهت تکمیل یا اصلاح کار شما patch یا وصله ارسال کنند.
و ...
اما برای استفاده از این امکانات نیاز است حداقل اطلاعاتی را در مورد کار با ابزارهای سورس کنترل داشت، که خلاصهی مختصر و مفید آنرا در ادامه ملاحظه خواهید نمود:
0 - دریافت و نصب برنامهی TortoiseSVN
1- ثبت نام در سایت CodePlex
رایگان است.
2- ایجاد یک پروژهی جدید
که به همراه وارد کردن مشخصات اولیه آن است:
تنها نکتهی مهم آن انتخاب سورس کنترل Team foundation server و سپس Subversion است چون میخواهیم با استفاده از TortoiseSVN کار به روز رسانی اطلاعات را انجام دهیم.
3- انتخاب مجوز برای پروژه در برگهی License پروژه ایجاد شده
تا مجوزی را برای پروژه انتخاب نکنید، مجوز ارائهی عمومی آنرا نخواهید یافت. در مورد مقایسهی مجوزهای سورس باز لطفا به این مطلب مراجعه کنید.
4- checkout کردن سورس کنترل
ابتدا به برگهی source code پروژه مراجعه کرده و بر روی لینک subversion در کنار صفحه کلیک کنید.
در صفحهی باز شده مشخصات اتصال به مخزن کد را جهت به روز رسانی آن مشاهده خواهید نمود.
اکنون جهت استفاده از آن یک پوشهی مشخص را در سیستم خود برای قرار دادن فایلها و ارسال آن به مخزن کد ایجاد کنید. مثلا به نام SiteRepository . سپس جایی داخل این پوشه، کلیک راست کرده و گزینهی SVN Checkout را انتخاب کنید:
در صفحهی باز شده آدرس svn مربوط به پروژه خود را وارد نموده و بر روی Ok کلیک کنید:
در صفحهی بعدی باید نام کاربری و کلمهی عبور مرتبط با حساب کاربری سایت کدپلکس خود را وارد نمائید. همچنین بهتر است گزینهی به خاطر سپاری آنرا نیز برای سهولت کار در دفعات بعدی انتخاب کنید:
به این صورت یک پوشهی مخفی svn در اینجا تشکیل خواهد شد که اطلاعات مخزن کد را در خود نگهداری میکند و نباید آنرا حذف کرد، تغییر داد، یا جابجا کرد.
5- اضافه کردن فایلهای دلخواه به مخزن کد
برای اضافه کردن کدهای مورد نظر خود، آنها را به پوشهی SiteRepository فوق کپی کرده و سپس بر روی آنها کلیک راست نموده و گزینهی Add مربوط به TortoiseSVN را انتخاب کنید:
به این صورت تنها فایلهای مورد نظر جهت اضافه شدن به مخزن کد علامتگذاری خواهند شد (ایجاد پوشه و قرار دادن فایلها درون آنها نیز به همین ترتیب است):
اکنون برای تکمیل فرایند، جایی درون پوشه کلیک راست کرده و گزینهی SVN Commit را انتخاب کنید:
در صفحهی باز شده توضیحاتی را در مورد فایلهای ارسالی وارد کرده و سپس بر روی دکمهی OK کلیک نمائید:
پس از مدتی کار هماهنگ سازی اطلاعات با مخزن کد صورت خواهد گرفت:
همچنین آیکون فایلهای مورد نظر نیز بر روی کامپیوتر شما به صورت زیر تغییر خواهند کرد:
6- ارائه نهایی پروژه
فراموش نکنید که پس از ایجاد یک پروژهی جدید، انتخاب مجوز و ارسال فایلهای مورد نظر، باید بر روی دکمهی publish this project در بالای صفحه کلیک کرد. در غیراینصورت پروژهی شما در روز بعد به صورت خودکار از سایت CodePlex حذف میگردد:
برای نمونه مخزن جدید کدهای وبلاگ جاری را در آدرس زیر میتوانید مشاهده کنید:
در دفعات آتی، تنها تکرار مرحله 5 یعنی کپی کردن فایلهای مورد نظر به پوشهی SiteRepository، سپس Add و در نهایت Commit آنها کفایت میکند و نیازی به تکرار سایر مراحل نیست. عملیات هماهنگ سازی با مخزن کد هم بسیار بهینه است و تنها فایلهایی که اخیرا اضافه شده و هنوز ارسال نشدهاند، Commit خواهند شد.
کاربران نهایی هم یا از طریق اینترفیس تحت وب سایت میتوانند از فایلهای شما استفاده کنند و یا روش دیگری هم برای این منظور وجود دارد (همان Checkout کردن یاد شده و سپس هر بار انتخاب گزینهی SVN update بجای Commit جهت دریافت فایلهای جدید و نه کل مخزن کد به صورت یکجا).