insertHTML: { title: 'Insert Code', icon: { type: 'font', value: 'fa fa-dollar' // Font Awesome icon class fa fa-* }, callback: function (editor) { this.saveSelection(); var thisEditor = this; var codeModal = $("<div>").addClass("froala-modal").appendTo("body"); var wrapper = $("<div>").addClass("f-modal-wrapper").appendTo(codeModal); $("<h4>").append('<span data-text="true">Insert Code</span>') .append($('<i class="fa fa-times" title="Cancel">') .click(function () { codeModal.remove(); })) .appendTo(wrapper); var dialog = "<textarea id='code_area' style='height: 211px; width: 538px;' /><label>Language:</label><select id='code_lang'><option>CSharp</option><option>VB</option><option>JScript</option><option>Sql</option><option>XML</option><option>CSS</option><option>Java</option><option>Delphi</option></select> <input type='button' name='insert' id='insert_btn' value='Insert' />"; $(dialog).appendTo(wrapper); $("#code_area").text(this.text()); if (!this.selectionInEditor()) { this.$element.focus(); } $('#insert_btn').click(function () { var lang = $("#code_lang").val(); var code = $("#code_area").val(); code = code.replace(/\s+$/, ""); // rtrim code = $('<span/>').text(code).html(); // encode var htmlCode = "<pre class='brush: " + lang.toLowerCase() + "' language='" + lang + "' name='code'>" + code + "</pre></div>"; // syntaxhighlighter با این کد هماهنگ است //var htmlCode = "<pre language='" + lang + "' name='code'>" + code + "</pre></div>"; var codeBlock = "<div align='left' dir='ltr'>" + htmlCode + "</div>"; thisEditor.restoreSelection(); thisEditor.insertHTML(codeBlock); thisEditor.saveUndoStep(); codeModal.remove(); }); } }
راستش من منوهای چند سطحی پویا را برای bootstrap navbar نوشتم. الگوریتمش را مجبور شدم به صورت بازگشتی بنویسم.اگر کسی میتونه به صورت غیر بازگشتی بگه ممنونش میشم.
این کد یه partialpage برای navbar هست که هرکسی برای bootstrap به راحتی میتونه استفاده کنه.
@model IEnumerable<DomainClasses.Page> @helper ShowNavBar(IEnumerable<DomainClasses.Page> pages) { foreach (var page in pages) { if (page != null) { if (page.Children.Count == 0) { <text><li><a tabindex="-1" href="#">@page.Title</a></li></text> } if (page.Children.Count > 0 && page.Parent == null) { <text><li class="dropdown"><a class="dropdown-toggle" id="dLabel" role="button" data-toggle="dropdown" data-target="#" href="/page.html">@page.Title<b class="caret"></b></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li><a tabindex="-1" href="#">@page.Title</a></li></text> @ShowNavBar(page.Children) @:</ul></li> } if (page.Children.Count > 0 && page.Parent != null) { <text><li class="dropdown-submenu"><a tabindex="-1" href="#">@page.Title</a><ul class="dropdown-menu"></text> @ShowNavBar(page.Children) @:</ul></li> } } } } <div class="navbar" style="margin-bottom: 10px;"> <div class="navbar-inner"> <a class="brand" href="www.google.com">IT-EBOOK</a> <ul class="nav"> <li class="active"><a href="#">خانه</a></li> <li><a href="#">ورود</a></li> @ShowNavBar(Model) <li><a href="#">ارتباط با ما</a></li> </ul> <div class="input-append pull-left visible-desktop" style="margin-top: 5px;"> <input class="span6 search-input" id="Text1" type="text"> <button class="btn btn-primary" type="button">جست و جو</button> <button class="btn btn-info btn-advanced-search" type="button">پیشرفته</button> </div> </div> </div>
الان تنها مشکلم اینه که فیلدهای اضافی هم کوئری گرفته میشه.میدونم فیلدهای اضافی(بر اساس مدلی که ذکر کردم) را چگونه با استفاده از select حذف کنم اما توی viewmodel نمیدونم چه جوری children را از اطلاعات پر کنم؟
ممنون
[BreezeController] public class AccountController : ApiController { ... }
module Interfaces { export interface IAuthService { user: Models.IUserToken getUserInfo(accessToken); login(data); logOut(); register(data); changePassword(data); accessToken(accessToken, data); } }
سرویس AuthService پیاده سازی اینترفیس IAuthService را برعهده دارد. در سازنده آن، وابستگیهای آن مقداردهی شدهاست و همچنین تنظیمات manager را انجام دادهایم.
"grant_type=password & username=myusername & password=mypassword";
var ajaxAdapter = breeze.config.getAdapterInstance("ajax"); breeze.ajaxpost(ajaxAdapter);
.withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }
module AdApps { var securityUrls = { site: '/', login: '/token', logout: 'logout', register: 'register', userInfo: 'getUserInfo', changePassword: 'changePassword', } export class AuthService implements Interfaces.IAuthService { private manager: breeze.EntityManager; constructor( private _breeze: typeof breeze, private $http: ng.IHttpProvider, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { var dataService = new _breeze.DataService({ serviceName: "/breeze/Account", hasServerMetadata: false }); var metadataStore = new _breeze.MetadataStore({ namingConvention: _breeze.NamingConvention.camelCase }); this.manager = new _breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore, saveOptions: new _breeze.SaveOptions({ allowConcurrentSaves: true, tag: [{}] }) }); } user: Models.IUserToken; accessToken(accessToken, data): string { if (accessToken === 'clear') { localStorage.removeItem('accessToken'); delete this.$http.defaults.headers.common.Authorization; } else { window.localStorage.setItem("accessToken", accessToken); this.$http.defaults.headers.common.Authorization = 'Bearer ' + accessToken; } return accessToken; } getUserInfo(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.userInfo); return this.manager.executeQuery(query).then(data => { return data.results[0]; }); } login(data: any): ng.IPromise<any> { var newData = "grant_type=password&username=" + data.userName + "&password=" + data.password; var query = this._breeze.EntityQuery.from(securityUrls.login) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: newData }); return this.manager.executeQuery(query).then(data => { var self = this; var result = data.results[0] as any; self.accessToken(result.access_token, data.results[0]); self.user = <Models.IUserToken>{}; self.user = <Models.IUserToken>result; return result; }); } logOut(): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.logout) .withParameters({ $method: 'POST', $encoding: 'JSON', }); return this.manager.executeQuery(query).then(data => { this.user = null; this.accessToken('clear', null); this.$location.path("/"); }); } register(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.register) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } changePassword(data: Object): ng.IPromise<any> { var query = this._breeze.EntityQuery.from(securityUrls.changePassword) .withParameters({ $method: 'POST', $encoding: 'JSON', $data: data }); return this.manager.executeQuery(query).then(data => { }); } } }
سرویس HttpInterceptor : رهگیری و پیگیری کردن نتیجه درخواستهای http را بر عهده دارد.
module AdApps { export class HttpInterceptor { private static _toaster: ngtoaster.IToasterService; private static _$q: ng.IQService; constructor( private $q: ng.IQService, private toaster: ngtoaster.IToasterService, private $location: ng.ILocationService) { HttpInterceptor._toaster = toaster; HttpInterceptor._$q = $q; } request(config): string { config.headers = config.headers || {}; var authData = window.localStorage.getItem("accessToken"); if (authData) { config.headers.Authorization = "Bearer " + authData; } return config; }; response(response): ng.IPromise<any> { if (response.data && response.data.message && response.status === 200) { HttpInterceptor._toaster.success(response.data.message) } return HttpInterceptor._$q.resolve(response); }; responseError(response): ng.IPromise<any> { var self = this; var data = response.data; var title = "خطا"; var messages = []; if (data) { if (data.error) { title = data.error; } if (data.message) { messages.push(data.message); } if (data.Message) { messages.push(data.Message); } if (data.ModelState) { angular.forEach(data.ModelState, function (errors, key) { if (key.substr(0, 1) != "$") { messages.push(errors); } }); } if (data.exceptionMessage) { messages.push(data.exceptionMessage); } if (data.ExceptionMessage) { messages.push(data.ExceptionMessage); } if (data.error_description) { messages.push(data.error_description); } if (messages.length > 0) { HttpInterceptor._toaster.error(title, messages.join("<br/>")); } if (response.status === "401") { self.$location.path("/ورود"); } } return HttpInterceptor._$q.reject(response); } } }
معرفی کردن مسیرهای ورود، ثبت نام و تغییر رمز عبور به انگولار
module AdApps { class SecurityCtrl { constructor(private $scope: Interfaces.IAuthScope, private authService: AuthService) { $scope.authService = authService; if (window.localStorage.getItem("accessToken") != null) { authService.getUserInfo().then(function (data) { $scope.authService.user = data; }); } $scope.logOut = function () { return authService.logOut().then(function () { }); } } } define(["angularAmd", "angular", "factory/AuthService", "factory/httpInterceptor"], (angularAmd, ng) => { angularAmd = angularAmd.__proto__; var app = ng.module("AngularTypeScript", ['ngRoute', 'breeze.angular', 'toaster']); var viewPath = "app/views/"; var controllerPath = "app/controller/"; app.config(['$routeProvider', '$httpProvider', function ($routeProvider, $httpProvider) { $httpProvider.interceptors.push("HttpInterceptor"); $routeProvider .when("/", angularAmd.route({ templateUrl: viewPath + "home.html", controllerUrl: controllerPath + "home.js" })) .when("/login", angularAmd.route({ templateUrl: viewPath + "login.html", controllerUrl: controllerPath + "login.js" })) .when("/register", angularAmd.route({ templateUrl: viewPath + "register.html", controllerUrl: controllerPath + "register.js" })) .when("/changePassword", angularAmd.route({ templateUrl: viewPath + "change-password.html", controllerUrl: controllerPath + "changePassword.js" })) .otherwise({ redirectTo: '/' }); } ]); app.service('AuthService', ['breeze', '$http', 'toaster', '$location', AuthService]); app.service("HttpInterceptor", ["$q", "toaster", "$location", HttpInterceptor]); app.controller('SecurityCtrl', ['$scope', 'AuthService', SecurityCtrl]); return angularAmd.bootstrap(app); }); }
ایجاد کنترلر .login.ts و ارسال سرویسهای لازم به کلاس LoginCtrl
در صورت صحیح بودن نام کاربری و رمز عبور به صفحه اصلی هدایت خواهد شد.
module AdApps { define(['app'], function (app) { app.controller('LoginCtrl', ["$scope", "AuthService", "$location", LoginCtrl]); }); export class LoginCtrl { constructor($scope: Interfaces.ILoginScope, authService: AuthService, $location: ng.ILocationService) { $scope.submit = function () { authService.login(angular.copy($scope.form)) .then(function (data) { this.$location.path("/"); }) }; } } }
ایجاد login.html
<div ng-controller="LoginCtrl"> <div> <i></i> <span>ورود</span> <div> <div> </div> </div> </div> <div> <div> <div> <form name="Form" id="form1"> <fieldset> <div> <div> <input name="username" ng-model="form.userName" placeholder="نام کاربری" required> <span> <i></i> </span> </div> </div> <div> <div> <input name="password" type="password" ng-model="form.password" placeholder="{{'Password'}}" validator="required"> <span> <i></i> </span> </div> </div> </fieldset> <div> <button type="submit" ng-click="submit()">ورود</button> </div> </form> </div> </div> </div> </div>
requirejs.config({ paths: { "app": "app", "angularAmd": "/Scripts/angularAmd", "angular": "/Scripts/angular", "breezeAjaxpost": "/Scripts/breeze/breeze.ajaxpost", "breeze": "/Scripts/breeze/breeze.debug", "breezeAngular": "/Scripts/breeze/breeze.angular", "bootstrap": "/Scripts/bootstrap", "angularRoute": "/Scripts/angular-route", "jquery": "/Scripts/jquery-2.2.2", "entityManagerService": "factory/entityManagerService", "toaster": "/Scripts/toaster", }, waitSeconds: 0, shim: { "angular": { exports: "angular" }, "angularRoute": { deps: ["angular"] }, "bootstrap": { deps: ["jquery"] }, "breeze": { deps: ["jquery"] }, "breezeAngular": { deps: ["angular", "breeze"] }, "toaster": { deps: ["angular"] }, "app": { deps: ["bootstrap", "angularRoute", "toaster", "breezeAngular", "breezeAjaxpost"] } } }); require(["app"]);
که بعد از بررسی به راه حلهای زیر رسیدم:
راه حل اول
بعد از اجرای وب سرویس و باز کردن آدرس آن به صورت HTTPS در مرورگر، پیام مبنی بر عدم اعتبار گواهی HTTPS را در آدرس وارد شده، مشاهده میکنیم. (Untrusted certificate) (که نسبت به مرورگر استفاده شده، این پیام متفاوت است و من در اینجا از مرورگر IE استفاده میکنم)
- بر روی Certificate error در نوار آدرس، کلیک کرده و View certificates را انتخاب میکنیم.
- وقتی پنجره Certificate باز شد بر روی دکمه Install Certificate کلیک کرده و پنجره Certificate Import Wizard باز شده و Next را میزنیم و Place all certificates in the following store را انتخاب میکنیم و بر روی دکمه Browse کلیک میکنیم.
- از پنجره باز شده Trusted Root Certification Authorities را انتخاب میکنیم و بر روی دکمه OK، کلیک میکنیم.
- سپس Next را میزنیم و در پایان بر روی دکمه Finish کلیک میکنیم.
- پس از اتمام Wizard، پنجره Security Warning به شما نمایش داده میشود که باید بر روی Yes آن کلیک کنید، بعد از تایید، پیام .The import was successful به شما نمایش داده میشود.
راه حل دوم
ممکن است کامپیوتر شما با توسعه دهندگان دیگر که با حساب کاربری خود وارد میشوند، مشترک باشد و بخواهید اطلاعات مربوط به گواهی اعتبار، به صورت مشترک استفاده شود. جزئیات در این روش بیشتر از روش قبل است.
- بازکردن پنجره Run و وارد کردن دستور mmc و زدن دکمه OK.
- اضافه کردن Snap-in
- انتخاب Add/Remove Snap-in از منوی File
- انتخاب Certificates از لیست سمت چپ و انتخاب دکمه Add
- در پنجره Certificates Snap-ins انتخاب گزینه Computer account و انتخاب دکمه Next
- انتخاب Local computer و کلیک بر روی دکمه Finish
- انتخاب دکمه OK
- استخراج IIS Express certificate از computer’s personal store
- در قسمت Console Root ، بخش Certificates (Local Computer)، سپس قسمت Personal و انتخاب Certificates.
- انتخاب گواهی با مشخصات زیر:
- "Issued to = "localhost
- "Issued by = "localhost
- "Friendly Name = "IIS Express Development Certificate
- انتخاب گزینه Export از زیرمنوی All Tasks در منوی Action
- پنجره Certificate Export Wizard باز شده و انتخاب دکمه Next
- انتخاب No, do not export the private key و انتخاب دکمه Next
- انتخاب DER encoded binary X.509 (.CER) و انتخاب دکمه Next
- انتخاب مسیر ذخیره فایل گواهی تصدیق مجوز و انتخاب دکمه Next
- انتخاب دکمه Finish برای انجام عملیات Export و مشاهده پیام موفقیت
- وارد کردن IIS Express certificate به computer’s Trusted Root Certification Authorities store
- در قسمت Console Root ، بخش Certificates (Local Computer)، سپس قسمت Trusted Root Certification Authorities و انتخاب Certificates.
- انتخاب گزینه Import از زیرمنوی All Tasks در منوی Action
- پنجره Certificate Export Wizard باز شده و انتخاب دکمه Next
- انتخاب مسیر فایل ذخیره شده در مرحله قبل و انتخاب دکمه Next
- انتخاب Place all certificates in the following store و در قسمت Certificate store ، انتخاب بخش Trusted Root Certification Authorities و انتخاب دکمه Next
- انتخاب دکمه Finish برای انجام عملیات Import و مشاهده پیام موفقیت و مشاهده گواهی تصدیق مجوز با نام localhost در لیست Trusted Root Certification Authorities
راه حل سوم
با استفاده از Developer Command Prompt نیز میتوان این کار را انجام داد.
- با اجرای دستور زیر و دریافت فایل خروجی
makecert -r -n "CN=localhost" -b 01/01/2000 -e 01/01/2099 -eku 1.3.6.1.5.5.7.3.1 -sv localhost.pvk localhost.cer cert2spc localhost.cer localhost.spc pvk2pfx -pvk localhost.pvk -spc localhost.spc -pfx localhost.pfx
- اجرای فایل localhost.pfx و وقتی پنجره Certificate Import Wizard باز شد، Next را میزنیم.
- نام فایل انتخاب شده را در این قسمت مشاهده میکنیم و Next را میزنیم.
- در صورت داشتن کلمه عبور، آن را وارد کرده (که در اینجا کلمه عبوری را تعریف نکردهایم) و Next را میزنیم.
- صفحه Place all certificates in the following store را انتخاب میکنیم و بر روی دکمه Browse کلیک میکنیم.
- از پنجره باز شده، Trusted Root Certification Authorities را انتخاب میکنیم و بر روی دکمه OK، کلیک میکنیم.
- سپس Next را میزنیم و در پایان بر روی دکمه Finish کلیک میکنیم.
- پس از اتمام Wizard، پنجره Security Warning به شما نمایش داده میشود که باید بر روی Yes کلیک کنید. بعد از تایید، پیام .The import was successful به شما نمایش داده میشود.
نکته: در صورتی که بخواهید برنامه شما (windows form) بتواند به سرور از طریق HTTPS اتصال پیدا کند، باید این فایل pfx بر روی هر کلاینت نصب شده باشد. شما میتوانید با اجرای دستور زیر در ابتدای فایل program.cs این کار را انجام دهید.
var cert = new X509Certificate2( Properties.Resources.localhost ); var store = new X509Store( StoreName.AuthRoot, StoreLocation.LocalMachine ); store.Open(OpenFlags.ReadWrite); store.Add(cert); store.Close();
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
<img [src]='producr.imageUrl'>
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
<img src={{producr.imageUrl}}>
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
<img src='http://www.mysite.com/images/{{producr.imageUrl}}'>
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName'> </td> <td>{{ product.productName }}</td> <td>{{ product.productCode }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price }}</td> <td>{{ product.starRating }}</td> </tr> </tbody>
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; products: any[] = [ // as before... ]; }
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
<tbody> <tr *ngFor='#product of products'> <td> <img [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
همچنین در اینجا نحوهی style binding را نیز مشاهده میکنید. مقصد اتصال همیشه با [] مشخص میشود و سپس کار با ذکر .style شروع شده و پس از آن نام خاصیت مدنظر عنوان خواهد شد. اگر نیاز به ذکر واحدی وجود داشت، پس از درج نام خاصیت، قید خواهد شد. برای مثال [style.fontSize.em] و یا [%.style.fontSize]
یک نکته:
اگر مثال را قدم به قدم دنبال کرده باشید، با افزودن style binding و بارگذاری مجدد صفحه، احتمالا تغییراتی را مشاهده نخواهید کرد. این مورد به علت کش شدن قالب قبلی و یا فایل جاوا اسکریپتی متناظر با آن است (فایلی که خواص عرض و حاشیهی تصویر به آن اضافه شدهاند).
یک روش سادهی حذف کش آن، بازکردن آدرس http://localhost:2222/app/products/product-list.component.js در مرورگر به صورت مجزا و سپس فشردن دکمههای ctrl+f5 بر روی آن است.
پاسخ دادن به رخدادها و یا event binding
تا اینجا تمام data bindingهای تعریف شدهی ما یک طرفه بودند؛ از خواص کلاس کامپوننت به اجزای قالب متناظر با آن. اما گاهی از اوقات نیاز است تا با کلیک کاربر بر روی دکمهای، عملی خاص صورت گیرد و در این حالت، جهت ارسال اطلاعات، از قالب کامپوننت، به متدها و خواص کلاس متناظر با آن خواهند بود. کامپوننت به اعمال کاربر از طریق event binding گوش فرا میدهد:
<button (click)='toggleImage()'>
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد toggleImage موجود در کلاس متناظر، فراخوانی خواهد شد.
چه رخدادهایی را در اینجا میتوان ذکر کرد؟ پاسخ آنرا در آدرس ذیل میتوانید مشاهده کنید:
https://developer.mozilla.org/en-US/docs/Web/Events
این syntax جدید AngularJS 2.0 سطح API آنرا کاهش داده است. دیگر در اینجا نیازی نیست تا به ازای هر رخداد ویژهای، یک دایرکتیو و یا syntax خاص آنرا در مستندات آن
جستجو کرد. فقط کافی است syntax جدید (نام رخداد) را مدنظر داشته باشید.
تکمیل مثال نمایش لیست محصولات با فعال سازی دکمهی Show Image آن
در اینجا قصد داریم با کلیک بر روی دکمهی Show image، تصاویر موجود در ستون تصاویر، مخفی و یا نمایان شوند. برای این منظور خاصیت جدیدی را به نام showImage به کلاس ProductListComponent اضافه میکنیم. بنابراین فایل product-list.component.ts را گشوده و تغییر ذیل را به آن اعمال کنید:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false;
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
export class ProductListComponent { // as before ... toggleImage(): void { this.showImage = !this.showImage; } }
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> Show Image </button>
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
<tr *ngFor='#product of products'> <td> <img *ngIf='showImage' [src]='product.imageUrl' [title]='product.productName' [style.width.px]='imageWidth' [style.margin.px]='imageMargin'> </td>
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
<button class='btn btn-primary' (click)='toggleImage()'> {{showImage ? 'Hide' : 'Show'}} Image </button>
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
<input [(ngModel)]='listFilter' >
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
export class ProductListComponent { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart';
<div class='panel-body'> <div class='row'> <div class='col-md-2'>Filter by:</div> <div class='col-md-4'> <input type='text' [(ngModel)]='listFilter' /> </div> </div> <div class='row'> <div class='col-md-6'> <h3>Filtered by: {{listFilter}}</h3> </div> </div>
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
{{ product.productCode | lowercase }}
از pipes در property binding هم میتوان استفاده کرد:
[title]='product.productName | uppercase'
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
{{ product.price | currency | lowercase }}
بعضی از pipes، پارامتر هم قبول میکنند:
{{ product.price | currency:'USD':true:'1.2-2' }}
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
<td>{{ product.productName }}</td> <td>{{ product.productCode | lowercase }}</td> <td>{{ product.releaseDate }}</td> <td>{{ product.price | currency:'USD':true:'1.2-2'}}</td> <td>{{ product.starRating }}</td>
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
<Window> <Grid> <Label Content="Label" /> <Button Content="Button" /> </Grid> </Window>
Dependency Properties
خاصیتهای وابستگی همان خاصیتها یا property هایی هستند در ویندوزفرم با آنها سر و کله میزدید ولی در اینجا تفاوتهایی با پراپرتیهای قبلی وجود دارد که باعث ایجاد مزایای زیادی شده است.
اول اینکه بر خلاف پراپرتیهای ویندوز فرم که در خود فیلدهای تعیین شده همان کنترل ذخیره میشدند، در این روش کلید (نام پراپرتی) و مقدار آن داخل یک شیء دیکشنری قرار میگیرند که از شیء DependencyObject ارث بری شده است و این شیء والد یک متد با نام GetValue برای دریافت مقادیر دارد. مزیت این روش این است که بیخود و بیجهت مانند روش قبلی، ما فیلدهایی را تعریف نمیکنیم که شاید به نصف بیشتر آنها، حتی نیازی نداریم. در این حالت تنها فیلدهایی از حافظه را دریافت و ذخیره میکنیم که واقعا به آنها نیاز داریم. فیلدها یا مقادیر پیش فرض موقع ایجاد شیء در آن ذخیره میشوند.
دومین مزیت این روش خاصیت ارث بری مقادیر از عناصر بالاتر درخت منطقی است. موقعی که از طرف شما برای فرزندان این عنصر مقداری تعیین نشده باشد، سیستم به سمت گرهها یا عناصر بالا یا والد حرکت میکند و اولین عنصری را که مقدارش تنظیم شده باشد، برای فرزندان در نظر میگیرد. به این استراتژی یافتن یک مقدار، استراتژی Resolution میگویند.
سومین مزیت آن وجود یک سیستم اعلان یا گزارش آنی است. در صورتی که شما یک تابع callback را برای یک پراپرتی ست نمایید، با تغییر این پراپرتی تابع معرفی شده صدا زده خواهد شد.
جادوی پشت صحنه
مقادیر پراپرتیها در کلاسی استاتیک به اسم Dependency Property ذخیره میشوند که این ذخیره در حالت نام و مقدار است و مقدار آن شامل callback و مقدار پیش فرض است. شکل زیر نتیجهی شکل دقیقتری را نسبت به قبلی در هنگام پیمایش درخت منطقی به سمت بالا، نشان میدهد.
نحوهی تعریف یک خاصیت وابسته که باید به صورت ایستا تعریف شود به صورت زیر است و برای دریافت و درج مقدار جدید از یک پراپرتی معمولی کمک میگیریم:
// Dependency Property public static readonly DependencyProperty CurrentTimeProperty = DependencyProperty.Register( "CurrentTime", typeof(DateTime), typeof(MyClockControl), new FrameworkPropertyMetadata(DateTime.Now)); // .NET Property wrapper public DateTime CurrentTime { get { return (DateTime)GetValue(CurrentTimeProperty); } set { SetValue(CurrentTimeProperty, value); } }
در مورد خاصیتهای وابسته و کدنویسی آن ها در مطالب آینده بیشتر بحث خواهیم کرد.
کلیات پروژه
پیاده سازی
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First(); jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
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); }) } }]);
<!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>
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); }) }
<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
<label> Code:</label> <textarea id="redactor_insert_code_area" name="redactor_insert_code_area" style="height: 211px; width: 538px;" /> <label> Language:</label> <select id="redactor_insert_code_lang"> <option>CSharp</option> <option>VB</option> <option>JScript</option> <option>Sql</option> <option>XML</option> <option>CSS</option> <option>Java</option> <option>Delphi</option> </select> <br /> <input type="button" name="insert" id="redactor_insert_btn" value="%RLANG.insert%" />
showCodesPage: function () { this.modalInit('Insert Code', this.opts.path + '/plugins/paste_code.html', 600, $.proxy(function () { var sel = this.getSelection(); var currentCode = ''; this.opts.codeElement = false; if ($.browser.msie) { var parent = this.getParentNode(); if (parent.nodeName === 'PRE') { this.opts.codeElement = parent; currentCode = $(parent).text(); } else { if (this.oldIE()) { currentCode = sel.text; } else { currentCode = sel.toString(); } } } else { if (sel && sel.anchorNode && sel.anchorNode.parentNode.tagName === 'PRE') { this.opts.codeElement = sel.anchorNode.parentNode; currentCode = $(sel.anchorNode.parentNode).text(); } else { currentCode = sel.toString(); } } if (this.opts.codeElement) { $("#redactor_insert_btn").val("Update"); } if (currentCode) $('#redactor_insert_code_area').val(currentCode); $('#redactor_insert_code_area').focus(); $('#redactor_insert_btn').click($.proxy(this.insertCodesPage, this)); }, this)); }, insertCodesPage: function () { var lang = $("#redactor_insert_code_lang").val(); var code = $("#redactor_insert_code_area").val(); code = code.replace(/\s+$/, ""); //rtrim; code = $('<span/>').text(code).html(); // encode this.$editor.focus(); var preBlock; if (this.opts.codeElement) { preBlock = $(this.getParentNode()); } else { preBlock = $("<pre/>"); } preBlock.replaceWith(""); var htmlCode = "<pre language='" + lang + "' name='code'>" + code + "</pre></div>"; var codeBlock = "<div align='left' dir='ltr'>" + htmlCode + "</div><br/>"; this.execCommand('inserthtml', codeBlock); this.modalClose(); },
code: { title: 'Code', func: 'showCodesPage' },
body .redactor_toolbar li a.redactor_btn_code span { background: url(../img/code_red.png) no-repeat center; }
ASP.NET Web API فریم ورکی برای ساختن APIهای وب بر روی فریم ورک دات نت است. در این مقاله با استفاده از این فریم ورک، API وبی خواهیم ساخت که لیستی از محصولات را بر میگرداند. صفحه وب کلاینت، با استفاده از jQuery نتایج را نمایش خواهد داد.
یک پروژه Web API بسازید
در ویژوال استودیو 2013 پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را "ProductsApp" انتخاب کنید.
در دیالوگ New ASP.NET Project قالب Empty را انتخاب کنید و در قسمت "Add folders and core references for" گزینه Web API را انتخاب نمایید.
می توانید از قالب Web API هم استفاده کنید. این قالب با استفاده از ASP.NET MVC صفحات راهنمای API را خواهد ساخت. در این مقاله از قالب Empty استفاده میکنیم تا تمرکز اصلی، روی خود فریم ورک Web API باشد. بطور کلی برای استفاده از این فریم ورک لازم نیست با ASP.NET MVC آشنایی داشته باشید.
افزودن یک مدل
یک مدل (model) آبجکتی است که داده اپلیکیشن شما را معرفی میکند. ASP.NET Web API میتواند بصورت خودکار مدل شما را به JSON, XML و برخی فرمتهای دیگر مرتب (serialize) کند، و سپس داده مرتب شده را در بدنه پیام HTTP Response بنویسد. تا وقتی که یک کلاینت بتواند فرمت مرتب سازی دادهها را بخواند، میتواند آبجکت شما را deserialize کند. اکثر کلاینتها میتوانند XML یا JSON را تفسیر کنند. بعلاوه کلاینتها میتوانند فرمت مورد نظرشان را با تنظیم Accept header در پیام HTTP Request مشخص کنند.
بگذارید تا با ساختن مدلی ساده که یک محصول (product) را معرفی میکند شروع کنیم.
کلاس جدیدی در پوشه Models ایجاد کنید.
نام کلاس را به "Product" تغییر دهید، و خواص زیر را به آن اضافه کنید.
namespace ProductsApp.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک کنترلر
در Web API کنترلرها آبجکت هایی هستند که درخواستهای HTTP را مدیریت کرده و آنها را به اکشن متدها نگاشت میکنند. ما کنترلری خواهیم ساخت که میتواند لیستی از محصولات، یا محصولی بخصوص را بر اساس شناسه برگرداند. اگر از ASP.NET MVC استفاده کرده اید، با کنترلرها آشنا هستید. کنترلرهای Web API مشابه کنترلرهای MVC هستند، با این تفاوت که بجای ارث بری از کلاس Controller از کلاس ApiController مشتق میشوند.
کنترلر جدیدی در پوشه Controllers ایجاد کنید.
در دیالوگ Add Scaffold گزینه Web API Controller - Empty را انتخاب کرده و روی Add کلیک کنید.
در دیالوگ Add Controller نام کنترلر را به "ProductsController" تغییر دهید و روی Add کلیک کنید.
توجه کنید که ملزم به ساختن کنترلرهای خود در پوشه Controllers نیستید، و این روش صرفا قراردادی برای مرتب نگاه داشتن ساختار پروژهها است. کنترلر ساخته شده را باز کنید و کد زیر را به آن اضافه نمایید.
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } }
کنترلر ما دو متد برای دریافت محصولات تعریف میکند:
- متد GetAllProducts لیست تمام محصولات را در قالب یک <IEnumerable<Product بر میگرداند.
- متد GetProductById سعی میکند محصولی را بر اساس شناسه تعیین شده پیدا کند.
همین! حالا یک Web API ساده دارید. هر یک از متدهای این کنترلر، به یک یا چند URI پاسخ میدهند:
URI | Controller Method |
api/products/ | GetAllProducts |
api/products/id/ | GetProductById |
برای اطلاعات بیشتر درباره نحوه نگاشت درخواستهای HTTP به اکشن متدها توسط Web API به این لینک مراجعه کنید.
فراخوانی Web API با جاوا اسکریپت و jQuery
در این قسمت یک صفحه HTML خواهیم ساخت که با استفاده از AJAX متدهای Web API را فراخوانی میکند. برای ارسال درخواستهای آژاکسی و بروز رسانی صفحه بمنظور نمایش نتایج دریافتی از jQuery استفاده میکنیم.
در پنجره Solution Explorer روی نام پروژه کلیک راست کرده و گزینه Add, New Item را انتخاب کنید.
در دیالوگ Add New Item قالب HTML Page را انتخاب کنید و نام فایل را به "index.html" تغییر دهید.
حال محتوای این فایل را با لیست زیر جایگزین کنید.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Product App</title> </head> <body> <div> <h2>All Products</h2> <ul id="products" /> </div> <div> <h2>Search by ID</h2> <input type="text" id="prodId" size="5" /> <input type="button" value="Search" onclick="find();" /> <p id="product" /> </div> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script> <script> var uri = 'api/products'; $(document).ready(function () { // Send an AJAX request $.getJSON(uri) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); }); function formatItem(item) { return item.Name + ': $' + item.Price; } function find() { var id = $('#prodId').val(); $.getJSON(uri + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); } </script> </body> </html>
گرفتن لیستی از محصولات
برای گرفتن لیستی از محصولات، یک درخواست HTTP GET به آدرس "api/products/" ارسال کنید.
تابع getJSON یک درخواست آژاکسی ارسال میکند. پاسخ دریافتی هم آرایه ای از آبجکتهای JSON خواهد بود. تابع done در صورت موفقیت آمیز بودن درخواست، اجرا میشود. که در این صورت ما DOM را با اطلاعات محصولات بروز رسانی میکنیم.
$(document).ready(function () { // Send an AJAX request $.getJSON(apiUrl) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); });
گرفتن محصولی مشخص
برای گرفتن یک محصول توسط شناسه (ID) آن کافی است یک درخواست HTTP GET به آدرس "api/products/id/" ارسال کنید.
function find() { var id = $('#prodId').val(); $.getJSON(apiUrl + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); }
اجرای اپلیکیشن
اپلیکیشن را با F5 اجرا کنید. صفحه وب باز شده باید چیزی مشابه تصویر زیر باشد.
برای گرفتن محصولی مشخص، شناسه آن را وارد کنید و روی Search کلیک کنید.
اگر شناسه نامعتبری وارد کنید، سرور یک خطای HTTP بر میگرداند.
استفاده از F12 برای مشاهده درخواستها و پاسخ ها
هنگام کار با سرویسهای HTTP، مشاهدهی درخواستهای ارسال شده و پاسخهای دریافتی بسیار مفید است. برای اینکار میتوانید از ابزار توسعه دهندگان وب استفاده کنید، که اکثر مرورگرهای مدرن، پیاده سازی خودشان را دارند. در اینترنت اکسپلورر میتوانید با F12 به این ابزار دسترسی پیدا کنید. به برگه Network بروید و روی Start Capturing کلیک کنید. حالا صفحه وب را مجددا بارگذاری (reload) کنید. در این مرحله اینترنت اکسپلورر ترافیک HTTP بین مرورگر و سرور را تسخیر میکند. میتوانید تمام ترافیک HTTP روی صفحه جاری را مشاهده کنید.
به دنبال آدرس نسبی "api/products/" بگردید و آن را انتخاب کنید. سپس روی Go to detailed view کلیک کنید تا جزئیات ترافیک را مشاهده کنید. در نمای جزئیات، میتوانید headerها و بدنه درخواستها و پاسخها را ببینید. مثلا اگر روی برگه Request headers کلیک کنید، خواهید دید که اپلیکیشن ما در Accept header دادهها را با فرمت "application/json" درخواست کرده است.
اگر روی برگه Response body کلیک کنید، میتوانید ببینید چگونه لیست محصولات با فرمت JSON سریال شده است. همانطور که گفته شده مرورگرهای دیگر هم قابلیتهای مشابهی دارند. یک ابزار مفید دیگر Fiddler است. با استفاده از این ابزار میتوانید تمام ترافیک HTTP خود را مانیتور کرده، و همچنین درخواستهای جدیدی بسازید که این امر کنترل کاملی روی HTTP headers به شما میدهد.
قدمهای بعدی
ASP.NET MVC #21
var onSuccess = function(result) { // enable unobtrusive validation for the contents // that was injected into the <div id="result"></div> node $.validator.unobtrusive.parse("#result"); };