-
Currently it is not recommended to combine inline editing and form editing with treegrid, or the expanded column will not be editable.
PdfRpt-1.3.zip
- Removed the limitation of defining non duplicate column names. See DuplicateColumns sample for more info. - Added horizontal stack panel mode. See CharacterMap sample for more info. - Added pdfStamper to onFillAcroForm of PdfTemplate. See QuestionsAcroForm sample for more info. Added 6 new samples: - AccountingBalanceColumn - CharacterMap - CustomPriceNumber - DuplicateColumns - QuestionsAcroForm - QuestionsForm
PdfRpt-2.8.7z
- Added ShouldSkipRow, ShouldSkipFooter & ShouldSkipHeader events. See Events/EventsPdfReport.cs sample for more info. - Updated the project to use iTextSharp.5.5.5. - Improved: Font styles (size, color etc.) are not applied if HTML fragment is rendered and does not have an explicit font-family style. patch #17253. - Improved: Properly aligning the aggregate value, based on the column information. #2395
{ "compileOnSave": true, "compilerOptions": { "target": "es5", "module": "commonjs", "moduleResolution": "node", "noEmitOnError": true, "removeComments": false, "sourceMap": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "noImplicitAny": false, "suppressImplicitAnyIndexErrors": true }, "exclude": [ "node_modules", "wwwroot", "typings/main", "typings/main.d.ts", "bower_components" ] }
تنظیم خروجی متدهای اشیاء Mock شده
در انتهای قسمت قبل، آزمون واحد متد Accept، با شکست مواجه شد؛ چون متد Validate استفاده شده، همواره مقدار false را بر میگرداند:
_identityVerifier.Initialize(); var isValidIdentity = _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
در ادامه شیء Mock از نوع IIdentityVerifier را طوری تنظیم خواهیم کرد که بر اساس یک applicant مشخص، خروجی true را بازگشت دهد:
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void Accept() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address)) .Returns(true); var mockCreditScorer = new Mock<ICreditScorer>(); var processor = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); processor.Process(application); Assert.IsTrue(application.IsAccepted); } } }
به این ترتیب زمانیکه در متد Process کلاس LoanApplicationProcessor کار به بررسی هویت کاربر میرسد، اگر متد Validate آن با اطلاعات applicant مشخصی که تنظیم کردیم، یکی بود، متغیر isValidIdentity که حاصل بررسی identityVerifier.Validate_ است، به true مقدار دهی خواهد شد. برای بررسی آن یک break-point را در این نقطه قرار داده و آزمون واحد را در حالت دیباگ اجرا کنید.
البته هرچند اگر اکنون نیز این آزمایش واحد را مجددا بررسی کنیم، باز هم با شکست مواجه خواهد شد؛ چون مرحلهی بعدی بررسی، کار با سرویس ICreditScorer است که هنوز تنظیم نشدهاست:
_creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); if (_creditScorer.Score < MinimumCreditScore) { return application.IsAccepted; }
تطابق با آرگومانهای متدها در متدهای Mock شده
با تنظیمی که انجام دادیم، اگر متد Validate به مشخصات شیء applicant مشخص ما برسد، خروجی true را بازگشت میدهد. برای مثال اگر در این بین تنها نام شخص تغییر کند، خروجی بازگشت داده شده همان false خواهد بود. اما اگر این نام برای ما اهمیتی نداشت و قصد داشتیم با تمام نامهای متفاوتی که دریافت میکند، بازهم خروجی true را بازگشت دهد، میتوان از قابلیت argument matching کتابخانهی Moq و کلاس It آن استفاده کرد:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate( //applicant.Name, It.IsAny<string>(), applicant.Age, applicant.Address)) .Returns(true);
بدیهی است در این حالت باید سایر پارامترها دقیقا با مقادیر مشخص شده تطابق داشته باشند و اگر این موارد نیز اهمیتی نداشتند، میتوان به صورت زیر عمل کرد:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(); mockIdentityVerifier.Setup(x => x.Validate( //applicant.Name, It.IsAny<string>(), //applicant.Age, It.IsAny<int>(), //applicant.Address It.IsAny<string>() )) .Returns(true);
البته این نوع تنظیمات بیشتر برای حالات غیرمشخص مانند استفادهاز Guidها به عنوان پارامترها و مقادیر، میتواند مفید باشد.
تقلید متدهایی که پارامترهایی از نوع out دارند
اگر به اینترفیس IIdentityVerifier که در قسمت قبل معرفی شد دقت کنیم، یکی از متدهای آن دارای خروجی از نوع out است:
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Validate(string applicantName, int applicantAge, string applicantAddress, out bool isValid); // ... } }
//var isValidIdentity = _identityVerifier.Validate( // application.Applicant.Name, application.Applicant.Age, application.Applicant.Address); _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address, out var isValidIdentity);
var isValidOutValue = true; mockIdentityVerifier.Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address, out isValidOutValue));
اکنون اگر مجددا آزمون واحد متد Accept را اجرا کنیم، با موفقیت به پایان میرسد.
تقلید متدهایی که پارامترهایی از نوع ref دارند
اگر به اینترفیس IIdentityVerifier که در قسمت قبل معرفی شد دقت کنیم، یکی از متدهای آن دارای خروجی از نوع ref است:
using Loans.Models; namespace Loans.Services.Contracts { public interface IIdentityVerifier { void Validate(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status); // ... } }
namespace Loans.Models { public class IdentityVerificationStatus { public bool Passed { get; set; } } }
IdentityVerificationStatus status = null; _identityVerifier.Validate( application.Applicant.Name, application.Applicant.Age, application.Applicant.Address, ref status); if (!status.Passed) { return application.IsAccepted; }
ابتدا در سطح کلاس آزمایش واحد یک delegate را تعریف میکنیم:
delegate void ValidateCallback(string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status);
اکنون روش استفادهی از آن برای برپایی تنظیمات mocking متد Validate از نوع ref دار به صورت زیر است:
mockIdentityVerifier .Setup(x => x.Validate(applicant.Name, applicant.Age, applicant.Address, ref It.Ref<IdentityVerificationStatus>.IsAny)) .Callback(new ValidateCallback( (string applicantName, int applicantAge, string applicantAddress, ref IdentityVerificationStatus status) => status = new IdentityVerificationStatus {Passed = true}));
سپس متد جدید Callback را مشاهده میکنید. توسط آن میتوان یک قطعه کد سفارشی را زمانیکه متد Mock شدهی Validate ما اجرا میشود، اجرا کرد. در اینجا delegate سفارشی ما اجرا شده و مقدار status را بر میگرداند؛ اما در ادامه این مقدار را به یک new IdentityVerificationStatus سفارشی تنظیم میکنیم که در آن مقدار خاصیت Passed، مساوی true است.
اکنون اگر مجددا آزمون واحد متد Accept را اجرا کنیم، با موفقیت به پایان میرسد.
تنظیم متدهای Mock شده جهت بازگشت null
فرض کنید اینترفیسی به صورت زیر تعریف شدهاست:
namespace Loans.Services.Contracts { public interface INullExample { string SomeMethod(); } }
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void NullReturnExample() { var mock = new Mock<INullExample>(); mock.Setup(x => x.SomeMethod()); //.Returns<string>(null); string mockReturnValue = mock.Object.SomeMethod(); Assert.IsNull(mockReturnValue); } } }
الف) میتوان همانند سابق متد Returns را ذکر کرد که نال بر میگرداند؛ اما با این تفاوت که حتما باید نوع آرگومان جنریک آنرا نیز بر اساس خروجی متد، مشخص کرد.
ب) کتابخانهی Moq، مقدار خروجی پیشفرض تمام متدهایی را که یک نوع ارجاعی را باز میگردانند، نال درنظر میگیرد و عملا نیازی به ذکر متد Returns در اینجا نیست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MoqSeries-02.zip
هر ردیف از جدول نمایانگر یک فایل یا پوشه است، بنابراین به ستونی نیاز داریم که بتوانیم این موضوع را نشان دهیم. بر این پایه از ستون is_directory بهره میبریم که 1 بودن آن نشاندهندهی این است که این ردیف از جدول به یک پوشه ارجاع دارد.
نام فایل یا پوشه در ستونی به نام name نگهداری میشود که رشتهای از نوع (nvarchar(255 است. افزون بر این ستون، ستونهای دیگری نیز در این جدول وجود دارد که ویژگیهای یک فایل مانند پنهانبودن، فقطخواندنی و ... توسط آن توسط آن به دست میآید. ستون پسین file_stream نام دارد که برای پوشهها، محتوای آن Null است. علت آن این است که محتوای واقعی فایل در این ستون نگهداری میشود. در واقع یک (varbinary(max با ویژگیهای fileStream است که محتوای باینری آن در سیستم فایل NTFS ذخیره میشود. مدیریت پشت صحنهی این ستون برعهدهی SQL Server است.
افزون بر این 14 ستون، هر FileTable شامل سه ستون محاسباتی به شرح زیر است:
ستون parent_path_locator نتیجهی فراخوانی تابع (GetAncestor(1 در ستون path_locator است که جهت به دست آوردن پوشهی پدر یک فایل و پوشه استفاده میشود. ستون file_type که از مقدار رشتهای ستون name تجزیه شده است، پسوند فایل را برمیگرداند. و ستون cached_file_size اندازهی بایت ذخیرهشده ستون file_stream را برمیگرداند. با این ساختار ثابت در اینجا، هر FileTable هر آنچه از File System نیاز دارید در یک پوشهی اشتراکی به شما میدهد.
این یعنی نمایش بیواسطه FileTable به هر کاربر یا برنامه. به طوری که برای نمایش یا بهروزرسانی جدول میتوانید از روش استاندارد I/O مانند کشیدن و رهاکردن با Windows Explorer یا برنامهنویسی با System.IO.FileStream و APIهای ویندوز استفاده کنید. اینچنین:
- ایجاد یک فایل یا پوشه در سیستم فایل -> افزودن یک ردیف به جدول
- افزودن یک ردیف به جدول -> ایجاد یک فایل یا پوشه در سیستم فایل
با کپی فایلها در مسیر بالا، به صورت خودکار رکوردهای زیر در جدول PhotoTable در پایگاهدادهها افزوده میشود:
به طور خلاصه پیش از این برای افزودن به FileStream دو راه کار پیش رو داشتید. یکی استفاده از T-SQL و دیگر sqlFileStream اکنون SQL Server 2012 راه کار سوم را پیشنهاد میکند. استفاده از File System در این روش FileStream به طور خودکار پر میشود.
پیش از ساخت یک FileTable بیان این نکته دارای اهمیت است که با کپی فایلها و پوشهها هیچ چیز جدیدی به NTFS افزوده نمیشود بلکه محتوای فایل به FileStream افزوده میشود و SQL Server با بررسی همزمان FileStream و FileTable نمایشی از ردیفهای FileTable به صورت یک پوشهی اشتراکی نشان میدهد. این نکته پاسخی به این پرسش خواهد بود که آیا با استفاده از FileTable حجم پایگاهدادهها دو برابر خواهد شد و در نتیجه دشواریها و چالشهای نگهداری و پشتیبانی را پیش رو خواهیم داشت!؟ که پاسخ "خیر" خواهد بود.
ایجاد یک FileTable
پیش از این در همین تارنما، روش فعال کردن FileStream در SQL Server را آموزش دیده اید. اگر درست به خاطر داشته باشید، چیزی شبیه به دستورهای زیر بود:
CREATE DATABASE MyFileArchive ON PRIMARY (NAME = MyFileArchive_data, FILENAME = 'C:\Demo\MyFileArchive_data.mdf'), FILEGROUP FileStreamGroup CONTAINS FILESTREAM (NAME = PhotoFileLibrary_blobs, FILENAME = 'C:\Demo\MyFiles') LOG ON (NAME = PhotoFileLibrary_log, FILENAME = 'C:\Demo\MyFileArchive_log.ldf')
FileTable به FileStream متکی است؛ بر این پایه پیش از ایجاد یک FileTable باید FileStream را روی پایگاهدادهها فعال کنیم. این کار با یک تعریف درست توسط بند FILEGROUP…CONTAINS FILESTREAM انجام میشود.
برای ایجاد FileTable تنها کافی است که بند WITH FILESTREAM را به دستور CREATE DATABASE بیفزایید. (یا برای فعالکردن FileTable روی یک پایگاهدادهی ساخته شده بند SET FILESTREAM را در دستور ALTER DATABASE بنویسید.) در این بند، از DIRECTORY_NAME برای نامگذاری یک پوشه برای پایگاهدادهها استفاده میکنیم. این پوشه در یک پوشه ریشه به نام SQL Server instance نمایش داده خواهد شد. بخش دوم بند NON_TRANSACTED_ACCESS=FULL است که دسترسی غیرتراکنشی را فعال میکند. با این کار برای هر FileTable در پایگاه داده یک زیرپوشه درون پوشهای که به نام DIRECTORY_NAME نامگذاری شده است؛ ساخته میشود.
با توجه به آنچه گفته شد برای ایجاد یک پایگاهداده با امکان ساخت FileTable دستورهای زیر را اجرا کنید:
CREATE DATABASE MyFileArchive ON PRIMARY (NAME = MyFileArchive_data, FILENAME = 'C:\Demo\MyFileArchive_data.mdf'), FILEGROUP FileStreamGroup CONTAINS FILESTREAM (NAME = PhotoFileLibrary_blobs, FILENAME = 'C:\Demo\MyFiles') LOG ON (NAME = PhotoFileLibrary_log, FILENAME = 'C:\Demo\MyFileArchive_log.ldf') WITH FILESTREAM (DIRECTORY_NAME='FilesLibrary', NON_TRANSACTED_ACCESS=FULL)
USE MyFileArchive GO CREATE TABLE PhotoTable AS FileTable GO
دنباله دارد ...
همهی بخشها به درستی کار میکنید و من هم طبق نیاز خودم یک سفارشی سازی هم کردم ولی الان با یک مشکل رو به رو شدم خوشحال میشم یک راهنمایی به من کنید.
سایتی که من دارم آماده میکنم چند زبانه هستش. و من با ارور 403 مشکل دارم.
تنظیمات این بخش بدین گونه هست :
var defaults = { moreInfoDiv: '#MoreInfoDiv', progressDiv: '#Progress', loadInfoUrl: '/', loginUrl: '/login', errorHandler: null, completeHandler: null, noMoreInfoHandler: null };
قسمت loginUrl زمانی استفاده میشه که با خطای 403 رو به رو بشیم :
if (xhr.status == 403) { window.location = options.loginUrl; }
الان وقتی سایت چند زبانه باشه چطور میتونیم کاربر رو به مسیر درست هدایت کنیم ؟
من از کوکیها استفاده نمیکنم و از rout متوجه میشم که زبان جاری چه زبانی است. البته میشود که url رو چک کنم و متوجه بشم زبان جاری چی هستش ولی من میخوام توسط ویژگی AjaxOnly آدرس login رو به کلاینت ارسال کنم یعنی اینجا :
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public sealed class Mcv5AuthorizeAttribute : AuthorizeAttribute { #region Ctor public Mcv5AuthorizeAttribute(params string[] permissions) : base() { Roles = string.Join(",", permissions); } #endregion #region HandleUnauthorizedRequest protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAuthenticated) { filterContext.Result = new HttpStatusCodeResult(403); // throw new UnauthorizedAccessException(); //to avoid multiple redirects } else { HandleAjaxRequest(filterContext); base.HandleUnauthorizedRequest(filterContext); } } #endregion #region Private private static void HandleAjaxRequest(ControllerContext filterContext) { var ctx = filterContext.HttpContext; if (!ctx.Request.IsAjaxRequest()) return; ctx.Response.StatusCode = (int)HttpStatusCode.Forbidden; //برای درخواستهای اجکسی اعتبار سنجی نشده ctx.Response.End(); } #endregion }
از اینجا میشه اینکارو کرد ؟ اگر نمیشه لطفا یک راهی به من نشان بدید. ممنونم
OpenCVSharp #6
An unhandled exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll Additional information: Exception has been thrown by the target of an invocation.
private void button1_Click(object sender, EventArgs e) { if (_worker != null && _worker.IsBusy) { return; } _worker = new BackgroundWorker { WorkerReportsProgress = true, WorkerSupportsCancellation = true }; _worker.DoWork += workerDoWork; _worker.ProgressChanged += workerProgressChanged; _worker.RunWorkerCompleted += workerRunWorkerCompleted; _worker.RunWorkerAsync(); }
private void workerDoWork(object sender, DoWorkEventArgs e) { //var interval = (int)(1000 / _capture.Fps); Image image; while ((image = _capture.QueryFrame().ToBitmap()) != null && _worker != null && !_worker.CancellationPending) { _worker.ReportProgress(0, image); //Thread.Sleep(interval); Thread.Sleep(10); } } private void workerProgressChanged(object sender, ProgressChangedEventArgs e) { var image = e.UserState as Image; if (image == null) return; //Cv.Not(image, image); //_pictureBoxIpl1.RefreshIplImage(image); //_pictureBoxIpl1.Image=image; _pictureBoxIpl1.Invoke(new EventHandler(delegate { _pictureBoxIpl1.Image = image; })); } private void workerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { _worker.Dispose(); _worker = null; }
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{id:int}")] public IActionResult Get(int id) { return Ok(id); } }
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{id}")] public IActionResult Get(int id) { // id is 0 here if you pass string. return Ok(id); } }
1- Inline Constraints
app.UseMvc(routes => { routes.MapRoute("Values", "api/values/{id:int}", new { controller = "Values", action = "Get" }); });
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{name:minlength(2):maxlength(10):alpha}")] public IActionResult Get(string name) { return Ok(name); } }
2- MapRoute's Constraints Argument
app.UseMvc(routes => { routes.MapRoute( name: "Values", template: "api/values/{name}", defaults: new { controller = "Values", action = "Get" }, constraints: new { name = new CompositeRouteConstraint(new List<IRouteConstraint> { new AlphaRouteConstraint(), new MinLengthRouteConstraint(2), new MaxLengthRouteConstraint(10) }) }); });
ایجاد یک Constraint سفارشی
public class StartsWithConstraint : IRouteConstraint { public StartsWithConstraint(string startsWith) { if (string.IsNullOrWhiteSpace(startsWith)) throw new ArgumentNullException(nameof(StartsWith)); StartsWith = startsWith; } private string StartsWith { get; } public bool Match(HttpContext httpContext, IRouter route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); if (values == null) throw new ArgumentNullException(nameof(values)); if (!values.TryGetValue(parameterName, out var value) || value == null) return false; string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); return valueString.StartsWith(StartsWith); } }
services.Configure<RouteOptions>(opt => opt.ConstraintMap.Add("startsWith", typeof(StartsWithConstraint)));
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{name:minlength(2):maxlength(10):alpha:startsWith(Mo)}")] public IActionResult Get(string name) { return Ok(name); } }
تنظیم مسیریابی ماژولها
در اینجا نیازی به تنظیم base path نیست و این تنظیم تنها یکبار به ازای کل برنامه انجام میشود. همانطور که در قسمت قبل نیز عنوان شد، ماژول مسیریابی Angular و یا همان RouterModule، به همراه سرویسی برای دسترسی به امکانات آن، تنظیمات مسیریابی و یک سری دایرکتیو مانند routerLink، جهت تعامل با آن است. از آنجائیکه سرویس ماژول مسیریابی در فایل src\app\app-routing.module.ts تعریف و تنظیم شدهاست، باید اطمینان حاصل کرد که این سرویس تنها یکبار در طول عمر برنامه وهله سازی میشود و از آنجائیکه هر ماژول تنظیمات مجزای مسیریابی خود را خواهد داشت، دیگر نمیتوان از متد RouterModule.forRoot سراسری استفاده کرد و در اینجا باید از متد forChild این ماژول، جهت تعریف تنظیمات مسیریابیهای ماژولهای مختلف کمک گرفت. متد forChild نیز شبیه به همان آرایهی تنظیمات مسیریابی متد forRoot را دریافت میکند.
یک مثال: در ادامهی مثالی که در قسمت قبل به کمک Angular CLI ایجاد کردیم، ماژول جدید محصولات را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد میکنیم:
>ng g m product --routing
import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; const routes: Routes = []; @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule] }) export class ProductRoutingModule { }
سپس ProductRoutingModule به قسمت imports ماژول محصولات به صورت خودکار اضافه شدهاست:
import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ProductRoutingModule } from './product-routing.module'; @NgModule({ imports: [ CommonModule, ProductRoutingModule ], declarations: [] }) export class ProductModule { }
در ادامه کامپوننت جدید لیست محصولات را به این ماژول اضافه میکنیم:
>ng g c product/ProductList
installing component create src\app\product\product-list\product-list.component.css create src\app\product\product-list\product-list.component.html create src\app\product\product-list\product-list.component.spec.ts create src\app\product\product-list\product-list.component.ts update src\app\product\product.module.ts
import { ProductListComponent } from './product-list/product-list.component'; @NgModule({ imports: [ ], declarations: [ProductListComponent] }) export class ProductModule { }
اکنون که این ماژول جدید را به همراه یک کامپوننت نمونه در آن تعریف کردیم، برای افزودن مسیریابی به آن، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آرایهی Routes آنرا تکمیل میکنیم:
import { ProductListComponent } from './product-list/product-list.component'; const routes: Routes = [ { path: 'products', component: ProductListComponent } ];
در ادامه میخواهیم لینکی را به این مسیریابی جدید اضافه کنیم. در قسمت قبل منویی را به برنامه اضافه کردیم. به همین جهت به فایل src\app\app.component.html مراجعه کرده و routerLink جدیدی را به آن اضافه میکنیم:
<nav class="navbar navbar-default"> <div class="container-fluid"> <a class="navbar-brand">{{title}}</a> <ul class="nav navbar-nav"> <li> <a [routerLink]="['/home']">Home</a> </li> <li> <a [routerLink]="['/products']">Product List</a> </li> </ul> </div> </nav> <div class="container"> <router-outlet></router-outlet> </div>
در اینجا نیز نحوهی تعریف لینکها مانند قبل است و آرایهی تنظیمات پارامترهای لینک باید به مقدار خاصیت path تعریف شده اشاره کند.
اکنون دستور ng serve -o را صادر کنید تا برنامه در حافظه ساخته شده و در مرورگر نمایش داده شود. در ادامه اگر بر روی لینک لیست محصولات کلیک کنید، صفحهی ذیل را مشاهده خواهید کرد:
به این معنا که برنامه اطلاعی از این مسیریابی جدید نداشته و صفحهی یافت نشدن مسیریابی را که در قسمت قبل تنظیم کردیم، نمایش دادهاست. برای رفع این مشکل باید به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی کنیم:
import { ProductModule } from './product/product.module'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, ProductModule, AppRoutingModule ],
نکته 1: علت اینکه ProductModule را پیش از AppRoutingModule تعریف کردیم این است که AppRoutingModule دارای تعریف مسیریابی ** یا catch all است که در قسمت قبل آنرا جهت مدیریت مسیرهای یافت نشده به برنامه افزودیم. اگر ابتدا AppRoutingModule تعریف میشد و سپس ProductModule، هیچگاه فرصت به پردازش مسیریابیهای ماژول محصولات نمیرسید؛ چون مسیر ** پیشتر برنده شده بود.
نکته 2: میتوان در قسمت import متد RouterModule.forRoot را نیز مستقیما قرار داد (بجای AppRoutingModule). اگر این کار صورت گیرد، ابتدا مسیریابیهای موجود در ماژولها پردازش میشوند و در آخر مسیرهای موجود در RouterModule.forRoot صرفنظر از محل قرارگیری آن در این لیست بررسی خواهد شد (حتی اگر در ابتدای لیست قرار گیرد). هرچند جهت مدیریت بهتر برنامه، این متد به AppRoutingModule منتقل شدهاست. بنابراین اکنون «نکتهی 1» برقرار است.
انتخاب استراتژی مناسب نامگذاری مسیرها
هنگام کار کردن با تعدادی ویژگی مرتبط به هم قرار گرفتهی داخل یک ماژول، بهتر است روش نامگذاری مناسبی را برای تنظیمات مسیریابی آن درنظر گرفت تا مسیرهای تعیین شده علاوه بر زیبایی، وضوح بیشتری را نیز پیدا کنند. به علاوه این نامگذاری مناسب، گروه بندی مسیریابیها و lazy loading آنها را نیز سادهتر میکند.
استراتژی ابتدایی که به ذهن میرسد، نامگذاری هر مسیر بر اساس عملکرد آنها است مانند products برای نمایش لیست محصولات، product/:id برای نمایش جزئیات محصولی خاص که در اینجا id پارامتر مسیریابی است و productEdit/:id برای ویرایش جزئیات یک محصول مشخص. همانطور که مشاهده میکنید، هرچند این مسیرها متعلق به یک ماژول هستند، اما مسیرهای تعیین شدهی برای آنها اینگونه به نظر نمیرسد. بنابراین بهتر است تمام ویژگیهای قرار گرفتهی درون یک ماژول را با مسیر ریشهی یکسانی شروع کنیم. به این ترتیب نمایش لیست محصولات همان products باقی خواهد ماند اما برای نمایش جزئیات محصولی خاص از مسیر products/:id استفاده میکنیم (همان اسم جمع ریشهی مسیر؛ بجای اسم مفرد). اینبار مسیر ویرایش جزئیات یک محصول به صورت products/:id/edit تنظیم خواهد شد:
products products/:id products/:id/edit
فعالسازی یک مسیر با کدنویسی
تا اینجا نحوهی فعالسازی یک مسیر را با استفاده از دایرکتیو routerLink بررسی کردیم. اما گاهی از اوقات نیاز است تا بتوان با کدنویسی نیز کاربران را به مسیری خاص هدایت کرد. برای مثال پس از عملیات logout میخواهیم مجددا صفحهی اول سایت نمایش داده شود. برای اینکار از سرویس Router مسیریاب Angular کمک گرفته میشود. ابتدا آنرا در سازندهی یک کامپوننت تزریق کرده و سپس میتوان به قابلیتهای آن مانند استفادهی از متد navigate آن، در کدهای برنامه دسترسی یافت.
باید درنظر داشت که دایرکتیو routerLink نیز در پشت صحنه از همین متد navigate سرویس Router استفاده میکند. بنابراین تمام پارامترهای آن در متد navigate نیز قابل استفاده هستند. برای مثال زمانیکه تعداد پارامترهای routerLink یک مورد است، میتوان آرایهی آنرا به یک رشته خلاصه کرد. یک چنین قابلیتی با متد navigate نیز میسر است.
متد navigate تنها قسمتهایی از URL جاری را تغییر میدهد. اگر نیاز باشد تا کل آدرس تعویض شود، میتوان از متد دیگر سرویس Router به نام navigateByUrl استفاده کرد. این متد تمام URL segments موجود را با مسیر جدیدی جایگزین میکند. به علاوه برخلاف متد navigate، تنها یک رشته را به عنوان پارامتر میپذیرد.
در ادامه مثال جاری میخواهیم پیاده سازی ابتدایی login و logout را به برنامه اضافه کنیم. به همین منظور ابتدا ماژول جدید user را به همراه تنظیمات ابتدایی مسیریابی آن اضافه میکنیم:
>ng g m user --routing
همانند ماژول قبلی، نیاز است UserModule را به قسمت imports فایل src\app\app.module.ts نیز معرفی کنیم:
import { UserModule } from './user/user.module'; @NgModule({ declarations: [ ], imports: [ BrowserModule, FormsModule, HttpModule, ProductModule, UserModule, AppRoutingModule ],
سپس کامپوننت جدید لاگین را به ماژول user برنامه اضافه میکنیم:
>ng g c user/login
در ادامه به فایل src\app\user\user-routing.module.ts مراجعه کرده و مسیریابی جدیدی را به کامپوننت لاگین تعریف میکنیم:
import { LoginComponent } from './login/login.component'; const routes: Routes = [ { path: 'login', component: LoginComponent} ];
مرحلهی بعد، فعالسازی این مسیریابی است، با تعریف لینکی به آن. به همین جهت به فایل src\app\app.component.html مراجعه کرده و منوی برنامه را تکمیل میکنیم:
<nav class="navbar navbar-default"> <div class="container-fluid"> <a class="navbar-brand">{{title}}</a> <ul class="nav navbar-nav"> <li> <a [routerLink]="['/home']">Home</a> </li> <li> <a [routerLink]="['/products']">Product List</a> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li> <a [routerLink]="['/login']">Log In</a> </li> </ul> </div> </nav> <div class="container"> <router-outlet></router-outlet> </div>
تکمیل کامپوننت login و افزودن لینک logout
در ادامه میخواهیم یک فرم لاگین مقدماتی را پس از کلیک بر روی لینک لاگین نمایش دهیم و هدایت به صفحهی لیست محصولات را پس از لاگین و مخفی کردن لینک لاگین و نمایش لینک خروج را در این حالت پیاده سازی کنیم. برای این منظور ابتدا اینترفیس خالی کاربر را ایجاد میکنیم:
>ng g i user/user
export interface IUser { id: number; userName: string; isAdmin: boolean; }
پس از آن یک سرویس ابتدایی اعتبارسنجی کاربران را نیز اضافه خواهیم کرد:
>ng g s user/auth -m user/user.module
installing service create src\app\user\auth.service.spec.ts create src\app\user\auth.service.ts update src\app\user\user.module.ts
پس از ایجاد قالب ابتدایی فایل auth.service.ts آنرا به نحو ذیل تکمیل کنید:
import { IUser } from './user'; import { Injectable } from '@angular/core'; @Injectable() export class AuthService { currentUser: IUser; constructor() { } isLoggedIn(): boolean { return !this.currentUser; } login(userName: string, password: string): boolean { if (!userName || !password) { return false; } if (userName === 'admin') { this.currentUser = { id: 1, userName: userName, isAdmin: true }; return true; } this.currentUser = { id: 2, userName: userName, isAdmin: false }; return true; } logout(): void { this.currentUser = null; } }
سپس کامپوننت لاگین واقع در فایل src\app\user\login\login.component.ts را به نحو ذیل تکمیل کنید:
import { Router } from '@angular/router'; import { AuthService } from './../auth.service'; import { Component, OnInit } from '@angular/core'; import { NgForm } from '@angular/forms'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent implements OnInit { errorMessage: string; pageTitle = 'Log In'; constructor(private authService: AuthService, private router: Router) { } ngOnInit() { } login(loginForm: NgForm) { if (loginForm && loginForm.valid) { let userName = loginForm.form.value.userName; let password = loginForm.form.value.password; if (this.authService.login(userName, password)) { this.router.navigate(['/products']); } } else { this.errorMessage = 'Please enter a user name and password.'; }; } }
از AuthService برای اعتبارسنجی کاربر و لاگین او به سیستم استفاده میکنیم و از سرویس مسیریاب Angular جهت فراخوانی متد navigate آن به صفحهی مشاهدهی محصولات، پس از لاگین کاربر استفاده شدهاست.
اکنون میخواهیم قالب این کامپوننت را نیز تکمیل کنیم. پیش از آن به فایل src\app\user\user.module.ts مراجعه کرده و در قسمت imports آن FormsModule را نیز اضافه کنید:
import { FormsModule } from '@angular/forms'; @NgModule({ imports: [ CommonModule, FormsModule, UserRoutingModule ],
سپس فایل src\app\user\login\login.component.html را به نحو ذیل تغییر دهید:
<div class="panel panel-default"> <div class="panel-heading"> {{pageTitle}} </div> <div class="panel-body"> <form class="form-horizontal" novalidate (ngSubmit)="login(loginForm)" #loginForm="ngForm" autocomplete="off"> <fieldset> <div class="form-group" [ngClass]="{'has-error': (userNameVar.touched || userNameVar.dirty) && !userNameVar.valid }"> <label class="col-md-2 control-label" for="userNameId">User Name</label> <div class="col-md-8"> <input class="form-control" id="userNameId" type="text" placeholder="User Name (required)" required (ngModel)="userName" name="userName" #userNameVar="ngModel" /> <span class="help-block" *ngIf="(userNameVar.touched || userNameVar.dirty) && userNameVar.errors"> <span *ngIf="userNameVar.errors.required"> User name is required. </span> </span> </div> </div> <div class="form-group" [ngClass]="{'has-error': (passwordVar.touched || passwordVar.dirty) && !passwordVar.valid }"> <label class="col-md-2 control-label" for="passwordId">Password</label> <div class="col-md-8"> <input class="form-control" id="passwordId" type="password" placeholder="Password (required)" required (ngModel)="password" name="password" #passwordVar="ngModel" /> <span class="help-block" *ngIf="(passwordVar.touched || passwordVar.dirty) && passwordVar.errors"> <span *ngIf="passwordVar.errors.required"> Password is required. </span> </span> </div> </div> <div class="form-group"> <div class="col-md-4 col-md-offset-2"> <span> <button class="btn btn-primary" type="submit" style="width:80px;margin-right:10px" [disabled]="!loginForm.valid"> Log In </button> </span> <span> <a class="btn btn-default" [routerLink]="['/welcome']"> Cancel </a> </span> </div> </div> </fieldset> </form> <div class="has-error" *ngIf="errorMessage">{{errorMessage}}</div> </div> </div>
اکنون میخواهیم پس از ورود او، نام او را نمایش داده و همچنین دکمهی logout را بجای login در منوی بالای سایت نمایش دهیم. به همین جهت در قالب کامپوننت App که منوی برنامه در آن تنظیم شدهاست، نیاز است بتوانیم به سرویس Auth سفارشی دسترسی یافته و خروجی متد isLoggedIn آنرا بررسی کنیم. به همین منظور به فایل src\app\app.component.ts مراجعه کرده و آنرا به صورت ذیل تکمیل کنید:
import { Router } from '@angular/router'; import { AuthService } from './user/auth.service'; import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { pageTitle: string = 'Routing Lab'; constructor(private authService: AuthService, private router: Router) { } logOut(): void { this.authService.logout(); this.router.navigateByUrl('/welcome'); } }
پس از این تغییرات، اکنون میتوان قالب src\app\app.component.html را به نحو ذیل تکمیل کرد:
<nav class="navbar navbar-default"> <div class="container-fluid"> <a class="navbar-brand">{{title}}</a> <ul class="nav navbar-nav"> <li> <a [routerLink]="['/home']">Home</a> </li> <li> <a [routerLink]="['/products']">Product List</a> </li> </ul> <ul class="nav navbar-nav navbar-right"> <li *ngIf="authService.isLoggedIn()"> <a>Welcome {{ authService.currentUser.userName }}</a> </li> <li *ngIf="!authService.isLoggedIn()"> <a [routerLink]="['/login']">Log In</a> </li> <li *ngIf="authService.isLoggedIn()"> <a (click)="logOut()">Log Out</a> </li> </ul> </div> </nav> <div class="container"> <router-outlet></router-outlet> </div>
اکنون اگر برنامه را توسط دستور ng serve -o اجرا کنید، صفحهی لاگین و منوی بالای صفحه چنین شکلی را خواهد داشت:
پس از لاگین، لینک لاگین از منو حذف شده و سپس نام کاربری و لینک به logout نمایان میشوند.
اینبار اگر بر روی logout کلیک کنید، نام کاربری و لینک logout از صفحه حذف و مجددا لینک لاگین نمایش داده میشود.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: angular-routing-lab-01.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.