From LGPL to Polyform Noncommercial
From version 5, we have changed the license from LGPL to Polyform Noncommercial 1.0.0 - a license that permits noncommercial use only. In combination with the Polyform Noncommercial license we sell licenses for customers who use EPPlus in a commercial business. Since EPPlus has been around for a while, you might wonder why we have decided to do this change.
مجوز عمومی فایلهای اسکریپت اصلی آن MIT است و در هر نوع پروژهای قابل استفادهاست. مجوز تجاری هم دارد برای حالتیکه بخواهید کامپوننتهای ASP.NET آنرا بخرید که ... نیازی نیست (^).
3. Can be used in proprietary works The license policy allow you to use this piece of code even inside commercial (not open source) projects. So you can use this software without giving away your own (precious?) source code.
قبلا شرح مختصری در زمینه OpenID در اینجا گفته شد.
حال میخواهیم این امکان را در پروژه خود بکار ببریم، جهت این کار باید ابتدا یک پروژه ایجاد کرده و از کتابخانههای سورس باز موجود استفاده کرد.
1- ابتدا در ویژوال استودیو یا هر نرم افزار دیگر یک پروژه MVC ایجاد نمایید.
2- نوع Internet Application و برای View Engine سایت Razor را انتخاب نمایید.
3- کتابخانه DotNetOpenId سورس باز را میتوانید مستقیما از این آدرس دانلود نموده یا از طریق Package Manager Console و با نوشتن Install-Package DotNetOpenAuth به صورت آنلاین این کتابخانه را نصب نمایید.
4- مدلهای برنامه را مانند زیر ایجاد نمایید
5- در پروژه مربوطه یک Controller به نام AccountController ایجاد نمایید. و کدهای زیر را برای آنها وارد نمایید.
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Web.Mvc; using System.Web.Security; namespace OpenIDExample.Models { #region Models public class ChangePasswordModel { [Required] [DataType(DataType.Password)] [Display(Name = "Current password")] public string OldPassword { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "New password")] public string NewPassword { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm new password")] [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } public class LogOnModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } public class RegisterModel { [Display(Name = "OpenID")] public string OpenID { get; set; } [Required] [Display(Name = "User name")] public string UserName { get; set; } [Required] [DataType(DataType.EmailAddress)] [Display(Name = "Email address")] public string Email { get; set; } [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } #endregion Models #region Services // The FormsAuthentication type is sealed and contains static members, so it is difficult to // unit test code that calls its members. The interface and helper class below demonstrate // how to create an abstract wrapper around such a type in order to make the AccountController // code unit testable. public interface IMembershipService { int MinPasswordLength { get; } bool ValidateUser(string userName, string password); MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID); bool ChangePassword(string userName, string oldPassword, string newPassword); MembershipUser GetUser(string OpenID); } public class AccountMembershipService : IMembershipService { private readonly MembershipProvider _provider; public AccountMembershipService() : this(null) { } public AccountMembershipService(MembershipProvider provider) { _provider = provider ?? Membership.Provider; } public int MinPasswordLength { get { return _provider.MinRequiredPasswordLength; } } public bool ValidateUser(string userName, string password) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); return _provider.ValidateUser(userName, password); } public Guid StringToGUID(string value) { // Create a new instance of the MD5CryptoServiceProvider object. MD5 md5Hasher = MD5.Create(); // Convert the input string to a byte array and compute the hash. byte[] data = md5Hasher.ComputeHash(Encoding.Default.GetBytes(value)); return new Guid(data); } public MembershipCreateStatus CreateUser(string userName, string password, string email, string OpenID) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(password)) throw new ArgumentException("Value cannot be null or empty.", "password"); if (String.IsNullOrEmpty(email)) throw new ArgumentException("Value cannot be null or empty.", "email"); MembershipCreateStatus status; _provider.CreateUser(userName, password, email, null, null, true, StringToGUID(OpenID), out status); return status; } public MembershipUser GetUser(string OpenID) { return _provider.GetUser(StringToGUID(OpenID), true); } public bool ChangePassword(string userName, string oldPassword, string newPassword) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); if (String.IsNullOrEmpty(oldPassword)) throw new ArgumentException("Value cannot be null or empty.", "oldPassword"); if (String.IsNullOrEmpty(newPassword)) throw new ArgumentException("Value cannot be null or empty.", "newPassword"); // The underlying ChangePassword() will throw an exception rather // than return false in certain failure scenarios. try { MembershipUser currentUser = _provider.GetUser(userName, true /* userIsOnline */); return currentUser.ChangePassword(oldPassword, newPassword); } catch (ArgumentException) { return false; } catch (MembershipPasswordException) { return false; } } public MembershipCreateStatus CreateUser(string userName, string password, string email) { throw new NotImplementedException(); } } public interface IFormsAuthenticationService { void SignIn(string userName, bool createPersistentCookie); void SignOut(); } public class FormsAuthenticationService : IFormsAuthenticationService { public void SignIn(string userName, bool createPersistentCookie) { if (String.IsNullOrEmpty(userName)) throw new ArgumentException("Value cannot be null or empty.", "userName"); FormsAuthentication.SetAuthCookie(userName, createPersistentCookie); } public void SignOut() { FormsAuthentication.SignOut(); } } #endregion Services #region Validation public static class AccountValidation { public static string ErrorCodeToString(MembershipCreateStatus createStatus) { // See http://go.microsoft.com/fwlink/?LinkID=177550 for // a full list of status codes. switch (createStatus) { case MembershipCreateStatus.DuplicateUserName: return "Username already exists. Please enter a different user name."; case MembershipCreateStatus.DuplicateEmail: return "A username for that e-mail address already exists. Please enter a different e-mail address."; case MembershipCreateStatus.InvalidPassword: return "The password provided is invalid. Please enter a valid password value."; case MembershipCreateStatus.InvalidEmail: return "The e-mail address provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidAnswer: return "The password retrieval answer provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidQuestion: return "The password retrieval question provided is invalid. Please check the value and try again."; case MembershipCreateStatus.InvalidUserName: return "The user name provided is invalid. Please check the value and try again."; case MembershipCreateStatus.ProviderError: return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator."; case MembershipCreateStatus.UserRejected: return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator."; default: return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator."; } } } [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class ValidatePasswordLengthAttribute : ValidationAttribute, IClientValidatable { private const string _defaultErrorMessage = "'{0}' must be at least {1} characters long."; private readonly int _minCharacters = Membership.Provider.MinRequiredPasswordLength; public ValidatePasswordLengthAttribute() : base(_defaultErrorMessage) { } public override string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentCulture, ErrorMessageString, name, _minCharacters); } public override bool IsValid(object value) { string valueAsString = value as string; return (valueAsString != null && valueAsString.Length >= _minCharacters); } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { return new[]{ new ModelClientValidationStringLengthRule(FormatErrorMessage(metadata.GetDisplayName()), _minCharacters, int.MaxValue) }; } } #endregion Validation }
using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; using DotNetOpenAuth.Messaging; using DotNetOpenAuth.OpenId; using DotNetOpenAuth.OpenId.RelyingParty; using OpenIDExample.Models; namespace OpenIDExample.Controllers { public class AccountController : Controller { private static OpenIdRelyingParty openid = new OpenIdRelyingParty(); public IFormsAuthenticationService FormsService { get; set; } public IMembershipService MembershipService { get; set; } protected override void Initialize(RequestContext requestContext) { if (FormsService == null) { FormsService = new FormsAuthenticationService(); } if (MembershipService == null) { MembershipService = new AccountMembershipService(); } base.Initialize(requestContext); } // ************************************** // URL: /Account/LogOn // ************************************** public ActionResult LogOn() { return View(); } [HttpPost] public ActionResult LogOn(LogOnModel model, string returnUrl) { if (ModelState.IsValid) { if (MembershipService.ValidateUser(model.UserName, model.Password)) { FormsService.SignIn(model.UserName, model.RememberMe); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } // If we got this far, something failed, redisplay form return View(model); } // ************************************** // URL: /Account/LogOff // ************************************** public ActionResult LogOff() { FormsService.SignOut(); return RedirectToAction("Index", "Home"); } // ************************************** // URL: /Account/Register // ************************************** public ActionResult Register(string OpenID) { ViewBag.PasswordLength = MembershipService.MinPasswordLength; ViewBag.OpenID = OpenID; return View(); } [HttpPost] public ActionResult Register(RegisterModel model) { if (ModelState.IsValid) { // Attempt to register the user MembershipCreateStatus createStatus = MembershipService.CreateUser(model.UserName, model.Password, model.Email, model.OpenID); if (createStatus == MembershipCreateStatus.Success) { FormsService.SignIn(model.UserName, false /* createPersistentCookie */); return RedirectToAction("Index", "Home"); } else { ModelState.AddModelError("", AccountValidation.ErrorCodeToString(createStatus)); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePassword // ************************************** [Authorize] public ActionResult ChangePassword() { ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(); } [Authorize] [HttpPost] public ActionResult ChangePassword(ChangePasswordModel model) { if (ModelState.IsValid) { if (MembershipService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword)) { return RedirectToAction("ChangePasswordSuccess"); } else { ModelState.AddModelError("", "The current password is incorrect or the new password is invalid."); } } // If we got this far, something failed, redisplay form ViewBag.PasswordLength = MembershipService.MinPasswordLength; return View(model); } // ************************************** // URL: /Account/ChangePasswordSuccess // ************************************** public ActionResult ChangePasswordSuccess() { return View(); } [ValidateInput(false)] public ActionResult Authenticate(string returnUrl) { var response = openid.GetResponse(); if (response == null) { //Let us submit the request to OpenID provider Identifier id; if (Identifier.TryParse(Request.Form["openid_identifier"], out id)) { try { var request = openid.CreateRequest(Request.Form["openid_identifier"]); return request.RedirectingResponse.AsActionResult(); } catch (ProtocolException ex) { ViewBag.Message = ex.Message; return View("LogOn"); } } ViewBag.Message = "Invalid identifier"; return View("LogOn"); } //Let us check the response switch (response.Status) { case AuthenticationStatus.Authenticated: LogOnModel lm = new LogOnModel(); lm.OpenID = response.ClaimedIdentifier; //check if user exist MembershipUser user = MembershipService.GetUser(lm.OpenID); if (user != null) { lm.UserName = user.UserName; FormsService.SignIn(user.UserName, false); } return View("LogOn", lm); case AuthenticationStatus.Canceled: ViewBag.Message = "Canceled at provider"; return View("LogOn"); case AuthenticationStatus.Failed: ViewBag.Message = response.Exception.Message; return View("LogOn"); } return new EmptyResult(); } } }
6- سپس برای Action به نام LogOn یک View میسازیم، برای Authenticate نیازی به ایجاد View ندارد چون قرار است درخواست کاربر را به آدرس دیگری Redirect کند. سپس کدهای زیر را برای View ایجاد شده وارد میکنیم.
@model OpenIDExample.Models.LogOnModel @{ ViewBag.Title = "Log On"; } <h2> Log On</h2> <p> Please enter your username and password. @Html.ActionLink("Register", "Register") if you don't have an account. </p> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> <form action="Authenticate?ReturnUrl=@HttpUtility.UrlEncode(Request.QueryString["ReturnUrl"])" method="post" id="openid_form"> <input type="hidden" name="action" value="verify" /> <div> <fieldset> <legend>Login using OpenID</legend> <div class="openid_choice"> <p> Please click your account provider:</p> <div id="openid_btns"> </div> </div> <div id="openid_input_area"> @Html.TextBox("openid_identifier") <input type="submit" value="Log On" /> </div> <noscript> <p> OpenID is service that allows you to log-on to many different websites using a single indentity. Find out <a href="http://openid.net/what/">more about OpenID</a> and <a href="http://openid.net/get/">how to get an OpenID enabled account</a>.</p> </noscript> <div> @if (Model != null) { if (String.IsNullOrEmpty(Model.UserName)) { <div class="editor-label"> @Html.LabelFor(model => model.OpenID) </div> <div class="editor-field"> @Html.DisplayFor(model => model.OpenID) </div> <p class="button"> @Html.ActionLink("New User ,Register", "Register", new { OpenID = Model.OpenID }) </p> } else { //user exist <p class="buttonGreen"> <a href="@Url.Action("Index", "Home")">Welcome , @Model.UserName, Continue..." </a> </p> } } </div> </fieldset> </div> </form> @Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.") @using (Html.BeginForm()) { <div> <fieldset> <legend>Or Login Normally</legend> <div class="editor-label"> @Html.LabelFor(m => m.UserName) </div> <div class="editor-field"> @Html.TextBoxFor(m => m.UserName) @Html.ValidationMessageFor(m => m.UserName) </div> <div class="editor-label"> @Html.LabelFor(m => m.Password) </div> <div class="editor-field"> @Html.PasswordFor(m => m.Password) @Html.ValidationMessageFor(m => m.Password) </div> <div class="editor-label"> @Html.CheckBoxFor(m => m.RememberMe) @Html.LabelFor(m => m.RememberMe) </div> <p> <input type="submit" value="Log On" /> </p> </fieldset> </div> }
پس از اجرای پروژه صفحه ای شبیه به پایین مشاهده کرده و سرویس دهنده OpenID خاص خود را میتوانید انتخاب نمایید.
7- برای فعال سازی عملیات احراز هویت توسط FormsAuthentication در سایت باید تنطیمات زیر را در فایل web.config انجام دهید.
<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" timeout="2880" /> </authentication>
جهت مطالعات بیشتر ودانلود نمونه کدهای آماده میتوانید به لینکهای (^ و ^ و ^ و ^ و ^ و ^ و ^ ) مراجعه کنید.
کد کامل پروژه را میتوانید از اینجا دانلود نمایید.
منبع
شاید برای شما هم پیش آمده باشد که در یک برنامهی Angular بخواهید layoutهای مختلفی داشته باشید؛ مثلا هنگام لاگین، طبق عرف کار باید هدر و فوتر صفحه از بین بروند و فقط فرم لاگین نمایش داده شود و یا بخواهید هنگام لاگین، یک layout مخصوص پنل مدیریتی داشته باشید و یا …
قبل از شروع، فرض را بر آن میگیریم که حداقل نیازهای یک پروژهی Angular را آماده کرده اید. سپس یک پوشهی جدید را به نام layout میسازیم و layoutهای مربوطه را در آن ایجاد میکنیم. با دستور زیر یک کامپوننت جدید را که layout ما خواهد شد، با نام دلخواهی ایجاد میکنیم:
و همچنین یک کامپوننت دیگر را برای صفحهی اصلی به نام homelayout میسازیم:
در ادامه Loginlayout را باز کرده و تنظیمات زیر را لحاظ کنید:
در اینجا یک هدر و یک فوتر را ساخته و <router-outlet></router-outlet> را در آن قرار میدهیم که قسمت پویای ما خواهد شد.
اکنون وارد کامپوننت home layout شوید و دقیقا مانند قبل، تنظیمات دلخواه خود را انجام داده و همچنین <router-outlet></router-outlet> راهم درون جائیکه میخواهید به صورت پویا باشد بگذارید.
تا اینجا ما فقط layoutها را طراحی کردیم. در ادامه در ریشهی پروژه، سه کامپوننت را به نامهای Home , Login, About میسازیم. Home و About دارای یک قالب و Login هم داری قالب مخصوص به خود میباشد.
سپس وارد کامپوننت آغازین برنامه (app.component.html) شوید و در آن <router-outlet></router-outlet> را وارد کنید. در اینجا دیگر نیازی به نوشتن تگهای خاص دیگری نیست.
در ادامه به اصلیترین قسمت، یعنی مسیریابی میرسیم. وارد app.module.ts شوید و آن را به صورت زیر تنظیم کنید:
همانطورکه ملاحظه میکنید، مسیریابی بالا شامل مسیریابیهای تو در تویی است. در اینجا کامپوننتهای Home و About درون HomelayoutComponent بارگذاری میشوند و خود HomelayoutComponent نیز درون app.component.
همچنین برای اینکه مشخص شود کدام کامپوننت به عنوان کامپوننت پیشفرض نمایش داده شود، به صورت زیر عمل میکنیم:
به این روش میتوانید هر تعداد layout ایی را که میخواهید، ایجاد کنید.
کدهای کامل این مطلب را میتوانید از اینجا دریافت و یا به صورت آنی آزمایش کنید.
قبل از شروع، فرض را بر آن میگیریم که حداقل نیازهای یک پروژهی Angular را آماده کرده اید. سپس یک پوشهی جدید را به نام layout میسازیم و layoutهای مربوطه را در آن ایجاد میکنیم. با دستور زیر یک کامپوننت جدید را که layout ما خواهد شد، با نام دلخواهی ایجاد میکنیم:
ng g c Loginlayout
ng g c homelayout
در ادامه Loginlayout را باز کرده و تنظیمات زیر را لحاظ کنید:
<div style="width: 100%;height: 250px;background-color: aquamarine"> <h1>Header</h1> </div> <router-outlet></router-outlet> <div style="width: 100%;height: 250px;background-color: brown"> <h1>Foother</h1> </div>
اکنون وارد کامپوننت home layout شوید و دقیقا مانند قبل، تنظیمات دلخواه خود را انجام داده و همچنین <router-outlet></router-outlet> راهم درون جائیکه میخواهید به صورت پویا باشد بگذارید.
تا اینجا ما فقط layoutها را طراحی کردیم. در ادامه در ریشهی پروژه، سه کامپوننت را به نامهای Home , Login, About میسازیم. Home و About دارای یک قالب و Login هم داری قالب مخصوص به خود میباشد.
سپس وارد کامپوننت آغازین برنامه (app.component.html) شوید و در آن <router-outlet></router-outlet> را وارد کنید. در اینجا دیگر نیازی به نوشتن تگهای خاص دیگری نیست.
در ادامه به اصلیترین قسمت، یعنی مسیریابی میرسیم. وارد app.module.ts شوید و آن را به صورت زیر تنظیم کنید:
export const routes: Routes = [ { path: 'Loginlayout', component: LoginlayoutComponent , children: [ { path: 'Login', component: LoginComponent, pathMatch: 'full'} ] }, { path: 'Homelayout', component: HomelayoutComponent, children: [ { path: 'About', component: AbouComponent, pathMatch: 'full'}, { path: 'Home', component: HomeComponent, pathMatch: 'full'} ] } ];
همچنین برای اینکه مشخص شود کدام کامپوننت به عنوان کامپوننت پیشفرض نمایش داده شود، به صورت زیر عمل میکنیم:
path: '', component: HomelayoutComponent, children: [ { path: '', component:HomeComponent, pathMatch: 'full'} ]
کدهای کامل این مطلب را میتوانید از اینجا دریافت و یا به صورت آنی آزمایش کنید.
مفهوم «ابزارها» و یا «project tools» از نگارش اول NET Core. به همراه آن است؛ مانند تنظیم زیر در فایل csproj برنامهها:
که سبب فعالسازی ابزار dotnet ef میشود و توسط آن میتوان دستورات dotnet ef database update و یا dotnet ef migrations add را بر روی پروژهی جاری اجرا کرد. در این حالت برنامه dotnet.exe، هاست اجرایی این ابزارهای محلی و مختص به یک پروژه است.
این ایده نیز از npm و ابزارهای محلی و مختص به یک پروژهی آن گرفته شدهاست. اما npm امکان نصب این ابزارها را به صورت سراسری نیز دارد که امکان وجود linters ، test runners و یا development web servers را میسر کردهاست و در این حالت نیازی نیست یک چنین ابزارهایی را به ازای هر پروژه نیز یکبار نصب کرد.
معرفی ابزارهای سراسری در NET Core 2.1.
اگر SDK جدید NET Core 2.1 را نصب کرده باشید، پس از Build یک پروژهی مبتنی بر NET Core 2.0. (که توسط فایل global.json، شماره SDK آن محدود و مقید نشدهاست) یک چنین پیامهای اخطاری را مشاهده خواهید کرد:
یکی از ویژگیهای جدید NET Core 2.1 معرفی global tools یا ابزارهای سراسری آن است. هدف از آن، تهیه برنامههای کنسول مبتنی بر NET Core. است که توسط NuGet توزیع و به روز رسانی میشوند. توسعه دهندگان جاوا اسکریپت با یک چنین مفهومی تحت عنوان ابزارهای سراسری NPM آشنا هستند (NPM global tools)؛ همان سوئیچ g- که یک ابزار جاوا اسکریپتی را به صورت سراسری نصب میکند؛ مانند کامپایلر TypeScript.
پیامهای اخطار فوق نیز به این معنا هستند که دیگر نیازی نیست تا برای اجرای دستور dotnet watch run، حتما ابزار پروژهی Microsoft.DotNet.Watcher.Tools را به صورت دستی به تمام فایلهای csproj خود اضافه کنید. ابزار Watcher و یا EntityFrameworkCore.Tools اکنون جزو ابزارهای سراسری NET Core 2.1. هستند و بدون نیازی به افزودن ارجاع خاصی به آنها، هم اکنون در تمام پروژههای NET Core. شما قابل دسترسی و استفاده هستند. بنابراین ارجاعات مستقیم به آنها را حذف کنید؛ چون غیرضروری میباشند.
روش نصب ابزارهای سراسری در NET Core.
روش نصب ابزارهای سراسری NET Core. به صورت زیر است:
با این دستور، برنامهی فرضی example از نیوگت دریافت شده و ابتدا در یکی از دو پوشهی زیر، فایل فشرده شدهی آن باز خواهد شد:
و سپس در ویندوز، در مسیر زیر قرار خواهد گرفت (محل نصب نهایی):
این مسیر در لینوکس به صورت زیر است:
در حال حاضر برای عزل این برنامهها باید به یکی از این مسیرها مراجعه و آنها را دستی حذف کرد (در هر دو مسیر toolspkgs و tools باید حذف شوند).
یک نمونه از این ابزارها را که dotnet-dev-certs نام دارد، پس از نصب SDK جدید، در مکانهای یاد شده، خواهید یافت. کار این ابزار سراسری، تولید یک self signed certificate مخصوص برنامههای ASP.NET Core 2.1 است که پیشتر در مطلب «اجبار به استفادهی از HTTPS در حین توسعهی برنامههای ASP.NET Core 2.1» آنرا بررسی کردیم.
نکته 1: بر اساس تصویر فوق، در خط فرمان، دستور dotnet-dev-certs را صادر کنید. اگر پیام یافت نشدن این دستور یا ابزار را مشاهده کردید، به معنای این است که مسیر نصب آنها به PATH سیستم اضافه نشدهاست. با استفاده از دستورات ذیل میتوانید این مسیر را به PATH سیستم اضافه کنید:
نکته 2: اگر به این مسیرها دقت کنید، این ابزارها صرفا برای کاربر جاری سیستم نصب میشوند و مختص به او هستند؛ به عبارتی user-specific هستند و نه machine global.
روش ایجاد ابزارهای سراسری NET Core.
همانطور که عنوان شد، ابزارهای سراسری NET Core. در اصل برنامههای کنسول آن هستند. به همین جهت پس از نصب SDK جدید، در یک پوشهی جدید، دستور dotnet new console را اجرا کنید تا یک برنامهی کنسول جدید مطابق آن ایجاد شود. سپس فایل csproj آنرا به صورت زیر ویرایش کنید:
در اینجا دو خاصیت IsPackable و PackAsTool جدید بوده و مختص به ابزارهای سراسری NET Core. هستند. تنظیم همین دو خاصیت برای تبدیل یک برنامهی کنسول معمولی به ابزار سراسری کافی است.
پس از آن برای تهیهی یک بستهی نیوگت از آن، دستور زیر را اجرا کنید:
پس از ارسال فایل nupkg حاصل به سایت نیوگت، کاربران آن میتوانند توسط دستور زیر آنرا نصب کنند:
<ItemGroup> <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" /> </ItemGroup>
این ایده نیز از npm و ابزارهای محلی و مختص به یک پروژهی آن گرفته شدهاست. اما npm امکان نصب این ابزارها را به صورت سراسری نیز دارد که امکان وجود linters ، test runners و یا development web servers را میسر کردهاست و در این حالت نیازی نیست یک چنین ابزارهایی را به ازای هر پروژه نیز یکبار نصب کرد.
معرفی ابزارهای سراسری در NET Core 2.1.
اگر SDK جدید NET Core 2.1 را نصب کرده باشید، پس از Build یک پروژهی مبتنی بر NET Core 2.0. (که توسط فایل global.json، شماره SDK آن محدود و مقید نشدهاست) یک چنین پیامهای اخطاری را مشاهده خواهید کرد:
warning : Using DotNetCliToolReference to reference 'Microsoft.EntityFrameworkCore.Tools.DotNet' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK. warning : Using DotNetCliToolReference to reference 'Microsoft.DotNet.Watcher.Tools' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK.
پیامهای اخطار فوق نیز به این معنا هستند که دیگر نیازی نیست تا برای اجرای دستور dotnet watch run، حتما ابزار پروژهی Microsoft.DotNet.Watcher.Tools را به صورت دستی به تمام فایلهای csproj خود اضافه کنید. ابزار Watcher و یا EntityFrameworkCore.Tools اکنون جزو ابزارهای سراسری NET Core 2.1. هستند و بدون نیازی به افزودن ارجاع خاصی به آنها، هم اکنون در تمام پروژههای NET Core. شما قابل دسترسی و استفاده هستند. بنابراین ارجاعات مستقیم به آنها را حذف کنید؛ چون غیرضروری میباشند.
روش نصب ابزارهای سراسری در NET Core.
روش نصب ابزارهای سراسری NET Core. به صورت زیر است:
dotnet tool install -g example
%USERPROFILE%\.dotnet\toolspkgs (Windows) $HOME/.dotnet/toolspkgs (macOS/Linux)
%USERPROFILE%\.dotnet\tools
~/.dotnet/tools
در حال حاضر برای عزل این برنامهها باید به یکی از این مسیرها مراجعه و آنها را دستی حذف کرد (در هر دو مسیر toolspkgs و tools باید حذف شوند).
یک نمونه از این ابزارها را که dotnet-dev-certs نام دارد، پس از نصب SDK جدید، در مکانهای یاد شده، خواهید یافت. کار این ابزار سراسری، تولید یک self signed certificate مخصوص برنامههای ASP.NET Core 2.1 است که پیشتر در مطلب «اجبار به استفادهی از HTTPS در حین توسعهی برنامههای ASP.NET Core 2.1» آنرا بررسی کردیم.
نکته 1: بر اساس تصویر فوق، در خط فرمان، دستور dotnet-dev-certs را صادر کنید. اگر پیام یافت نشدن این دستور یا ابزار را مشاهده کردید، به معنای این است که مسیر نصب آنها به PATH سیستم اضافه نشدهاست. با استفاده از دستورات ذیل میتوانید این مسیر را به PATH سیستم اضافه کنید:
Windows PowerShell: setx PATH "$env:PATH;$env:USERPROFILE/.dotnet/tools" Linux/macOS: echo "export PATH=\"\$PATH:\$HOME/.dotnet/tools\"" >> ~/.bash_profile
نکته 2: اگر به این مسیرها دقت کنید، این ابزارها صرفا برای کاربر جاری سیستم نصب میشوند و مختص به او هستند؛ به عبارتی user-specific هستند و نه machine global.
روش ایجاد ابزارهای سراسری NET Core.
همانطور که عنوان شد، ابزارهای سراسری NET Core. در اصل برنامههای کنسول آن هستند. به همین جهت پس از نصب SDK جدید، در یک پوشهی جدید، دستور dotnet new console را اجرا کنید تا یک برنامهی کنسول جدید مطابق آن ایجاد شود. سپس فایل csproj آنرا به صورت زیر ویرایش کنید:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <IsPackable>true</IsPackable> <PackAsTool>true</PackAsTool> <TargetFramework>netcoreapp2.1</TargetFramework> </PropertyGroup> </Project>
پس از آن برای تهیهی یک بستهی نیوگت از آن، دستور زیر را اجرا کنید:
dotnet pack -c Release
dotnet tool install -g package-name
عموما در محیط کاری اگر شبکه ویندوزی و mail server مورد استفاده هم ms exchange باشد، به طور قطع از outlook برای انجام امور روزمره ارسال و دریافت ایمیل استفاده میشود.
طبق معمول هم مشکل ما تاریخ فارسی است! یکی از شرکتهای ایرانی که در اینباره محصولی را ارائه داده با hook کردن تاریخ ویندوز، هر جایی که تاریخی قرار است نمایش داده شود، آنرا فارسی میکند. این محصول دو ایراد دارد: الف) رایگان نیست! ب) این hook بر روی عملکرد سایر برنامهها تاثیرگذار است. برای مثال برنامههای دات نت تاریخ قمری را نمایش خواهند داد، بر روی عملکرد و کارآیی کلی سیستم تاثیر منفی دارد و مشکلاتی از این دست.
افزونه زیر بدون دستکاری تاریخ ویندوز، دو کار را در MS outlook 2007 انجام خواهد داد:
الف) اضافه کردن ستون "تاریخ دریافت" شمسی
ب) در متن دریافتی، تمام تاریخهای sent موجود را یافته و شمسی میکند
دریافت افزونه:
لطفا اینجا کلیک کنید.
تذکر مهم:
نصب دو بسته به روز رسانی سیستم و دات نت فریم ورک را پیش از نصب این افزونه فراموش نکنید.
/Post/14/افزونه-فارسی-به-پارسی-برای-word-2007
در کل برای من کار راه انداز بوده :)
در طی روزهای آتی، سورس کامل و نحوه برنامه نویسی آنرا بررسی خواهیم کرد.
توی کلاس Start چنین تنظیماتی رو دارم:
با این تنظیمات، زبان برنامه فقط با تغییر DefaultRequestCulture تغییر میکنه، توی تنظیمات بالا، زبان سایت فقط فارسی هست، حتی اگه lang به en تغییر کنه. آیا تنظیمات دیگری باید صورت بگیره؟
public class LanguageRouteConstraint : IRouteConstraint
{
public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.ContainsKey("lang"))
{
return false;
}
var lang = values["lang"].ToString();
var result = lang == "fa" || lang == "en";
return result;
}
}
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(o => o.ResourcesPath = "Resources"); services.Configure<RouteOptions>(options => { options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint)); }); services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_2) .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization();
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR"), }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR"), }, RequestCultureProviders = new List<IRequestCultureProvider>() { new RouteDataRequestCultureProvider() { UIRouteDataStringKey = "lang", RouteDataStringKey = "lang" } } }); app.UseMvc(routes => { routes.MapRoute( name: "LocalizedAreas", template: "{lang:lang}/{area:exists}/{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "LocalizedDefault", template: "{lang:lang}/{controller=Home}/{action=Index}/{id?}" ); routes.MapRoute( name: "default", template: "{*catchall}", defaults: new { controller = "Home", action = "RedirectToDefaultLanguage" }); });
بله پروژه از نوع Asp.net MVC است. بنده افزونه را در فولدر Plugins ایجاد کردم و سپس یک فولدر در داخل فولدر Plugins به نام Blog ساختم و پروژههای افزونه را به داخل آن انتقال دادم (مشکل این موقع به وجود آمد و دلیل آن را نمیدانم) ! با برگرداندن پروژهها به فولدر قبلی، متد RegisterArea هم کار کرد. ولی با این که من namespaces مربوط به Routing پروژهها را ست کردم ولی با این حال با کلیک بر روی منوی مربوط به افزونه ردایرکت میشود به صفحه اصلی پروژه.
و این هم لینک تولیدی برای افزونه
این کانفیگ مربوط به افزونه
public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "BlogArea_default", "BlogArea/{controller}/{action}/{id}", // تکمیل نام کنترلر پیش فرض new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) } ); }
Url = new UrlHelper(requestContext).Action("Index", "Home",new{area="BlogArea"})
کانفیگ مربوط به پروژه اصلی
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) } );
در قسمت قبل، نگاهی مقدماتی داشتیم به مبحث data binding. در ادامه، این مبحث را به همراه pipes، جهت اعمال تغییرات بر روی اطلاعات، پیگیری خواهیم کرد.
انقیاد به خواص یا property binding
قابلیت property binding این امکان را فراهم میکند که یکی از خواص المانهای HTML را به مقادیر دریافتی از کلاس کامپوننت، متصل کنیم:
در این مثال، خاصیت src المان تصویر، به آدرس تصویر یک محصول متصل شدهاست.
در حین تعریف property binding، مقصد اتصال، داخل براکتها قرار میگیرد و خاصیت مدنظر المان را مشخص میکند. منبع اتصال همیشه داخل "" در سمت راست علامت مساوی قرار میگیرد.
اگر اینکار را بخواهیم با interpolation معرفی شدهی در قسمت قبل انجام دهیم، به کد ذیل خواهیم رسید:
در اینجا نه از []، برای معرفی مقصد اتصال استفاده شدهاست و نه از "" برای مشخص سازی منبع اتصال. این نوع اتصال یک طرفه است (از منبع به مقصد).
خوب، در یک چنین مواردی property binding بهتر است یا interpolation؟
توصیهی کلی ترجیح property binding به interpolation است. اما اگر در اینجا نیاز به انجام محاسباتی بر روی عبارت منبع وجود داشت، باید از interpolation استفاده کرد؛ مانند:
تکمیل قالب کامپوننت لیست محصولات
اگر از قسمت قبل به خاطر داشته باشید، در فایل product-list.component.html، لیست پردازش شدهی توسط ngFor*، فاقد ستون نمایش تصاویر محصولات است. به همین جهت فایل یاد شده را گشوده و سپس با استفاده از property binding، دو خاصیت src و title تصویر را به منبع دادهی آن متصل میکنیم:
در این حالت اگر برنامه را اجرا کنیم به خروجی ذیل خواهیم رسید:
هرچند اینبار تصاویر محصولات نمایش داده شدهاند، اما اندکی بزرگ هستند. بنابراین در ادامه با استفاده از property binding، خواص style آنرا تنظیم خواهیم کرد. برای این منظور فایل product-list.component.ts را گشوده و به کلاس ProductListComponent، دو خاصیت imageWidth و imageMargin را اضافه میکنیم:
البته همانطور که پیشتر نیز ذکر شد، چون مقادیر پیش فرض این خواص عددی هستند، نیازی به ذکر صریح نوع number در اینجا وجود ندارد (type inference).
پس از تعریف این خواص، امکان دسترسی به آنها در قالب کامپوننت وجود خواهد داشت:
همانطور که مشاهده میکنید، چون خاصیتهای جدید تعریف شده، جزئی از خواص اصلی کلاس هستند و نه خواص اشیاء لیست محصولات، دیگر به همراه .product ذکر نشدهاند.
همچنین در اینجا نحوهی 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 گوش فرا میدهد:
syntax آن بسیار شبیه است به حالت property binding و در اینجا بجای [] از () جهت مشخص سازی رخدادی خاص از المان مدنظر استفاده میشود. سمت راست این انتساب، متدی است که داخل "" قرار میگیرد و این متد متناظر است با متدی مشخص در کلاس متناظر با کامپوننت جاری.
در این حالت اگر کاربر روی دکمهی تعریف شده کلیک کند، متد 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 را گشوده و تغییر ذیل را به آن اعمال کنید:
در اینجا خاصیت Boolean جدیدی به نام showImage با مقدار اولیهی false تعریف شدهاست. به این ترتیب تصاویر، در زمان اولین بارگذاری صفحه نمایش داده نخواهند شد.
سپس به انتهای کلاس، پس از تعاریف خواص، متد جدید toggleImage را اضافه میکنیم:
در کلاسهای TypeScript نیازی به ذکر صریح واژهی کلیدی function برای تعریف متدی وجود ندارد. این متد، خروجی هم ندارد، بنابراین نوع خروجی آن void مشخص شدهاست. در بدنهی این متد، وضعیت خاصیت نمایش تصویر معکوس میشود.
پس از این تغییرات، اکنون میتوان به قالب این کامپوننت یا فایل product-list.component.html مراجعه و event binding را تنظیم کرد:
در اینجا click به عنوان رخداد مقصد، مشخص شدهاست. سپس آنرا به متد toggleImage کلاس ProductListComponent متصل میکنیم.
خوب، تا اینجا اگر کاربر بر روی دکمهی show image کلیک کند، مقدار خاصیت showImage کلاس ProductListComponent با توجه به کدهای متد toggleImage، معکوس خواهد شد.
مرحلهی بعد، استفاده از مقدار این خاصیت، جهت مخفی و یا نمایان ساختن المان تصویر جدول نمایش داده شدهاست. اگر از قسمت قبل به خاطر داشته باشید، کار ngIf*، حذف و یا افزودن المانهای DOM است. بنابراین ngIf* را به المان تصویر جدول اضافه میکنیم:
با توجه به ngIf* تعریف شده، المان تصویر تنها زمانی به DOM اضافه خواهد شد که مقدار خاصیت showImage مساوی true باشد.
اکنون برنامه را اجرا کنید. در اولین بار اجرای صفحه، تصاویر ستون اول جدول، نمایش داده نمیشود. پس از کلیک بر روی دکمهی Show image، این تصاویر نمایان شده و اگر بار دیگر بر روی این دکمه کلیک شود، این تصاویر مخفی خواهند شد.
یک مشکل! در هر دو حالت نمایش و مخفی سازی تصاویر، برچسب این دکمه Show image است. بهتر است زمانیکه قرار است تصاویر مخفی شوند، برچسب hide image نمایش داده شود و برعکس. برای حل این مساله از interpolation به نحو ذیل استفاده خواهیم کرد:
در اینجا اگر مقدار خاصیت showImage مساوی true باشد، مقدار رشتهای Hide و اگر false باشد، مقدار رشتهای show بجای {{}} درج خواهد شد.
بررسی انقیاد دو طرفه یا two-way binding
تا اینجا، اتصال مقدار یک خاصیت عمومی کلاس متناظر با قالبی، به اجزای مختلف آن، یک طرفه بودند. اما در ادامه نیاز است تا بتوان برای مثال در textbox قسمت filter by مثال جاری بتوان اطلاعاتی را وارد کرد و سپس بر اساس آن ردیفهای جدول نمایش داده شده را فیلتر نمود. این عملیات نیاز به انقیاد دو طرفه یا two-way data binding دارد.
برای تعریف انقیاد دو طرفه در AngularJS 2.0 از دایرکتیو توکاری به نام ngModel استفاده میشود:
ابتدا [] ذکر میشود تا مشخص شود که این عملیات در اصل یک property binding است؛ از خاصیت عمومی به نام listFilter به المان textbox تعریف شده.
سپس () تعریف شدهاست تا event binding را نیز گوشزد کند. کار آن انتقال تعاملات کاربر، با المان رابط کاربری جاری، به خاصیت عمومی کلاس یا همان listFilter است.
در اینجا ممکن است که فراموش کنید [()] صحیح است یا ([]) . به همین جهت به این syntax، نام banana in the box را دادهاند یا «موز درون جعبه»! موز همان event binding است که داخل جعبهی property binding قرار میگیرد!
خوب، برای اعمال انقیاد دو طرفه، به مثال جاری، فایل product-list.component.ts را گشوده و خاصیت رشتهای listFilter را به آن اضافه میکنیم:
سپس فایل قالب product-list.component.html را گشوده و انقیاد دو طرفه را به آن اعمال میکنیم:
در اینجا انقیاد دو طرفه، توسط ngModel، به خاصیت listFilter کلاس، در المان input تعریف شده، صورت گرفته است. سپس توسط interpolation مقدار این تغییر را در قسمت Filtered by به صورت یک برچسب نمایش میدهیم.
پس از اجرای برنامه، تکست باکس تعریف شده، مقدار اولیهی cart را خواهد داشت و اگر آنرا تغییر دهیم، بلافاصله این مقدار تغییر یافته را در برچسب Filtered by میتوان مشاهده کرد. به این رخداد two-way binding میگویند.
البته هنوز کار فیلتر لیست محصولات در اینجا انجام نمیشود که آنرا در قسمت بعد تکمیل خواهیم کرد.
فرمت کردن اطلاعات نمایش داده شدهی در جدول با استفاده از Pipes
تا اینجا لیست محصولات نمایش داده شد، اما نیاز است برای مثال فرمت ستون نمایش قیمت آن بهبود یابد. برای این منظور، از ویژگی دیگری به نام pipes استفاده میشود و کار آنها تغییر دادهها، پیش از نمایش آنها است. AngularJS 2.0 به همراه تعدادی pipe توکار برای فرمت مقادیر است؛ مانند date، number، decimal، percent و غیره. همچنین امکان ساخت custom pipes نیز پیش بینی شدهاست.
در اینجا یک مثال سادهی pipes را مشاهده میکنید:
پس از قید نام خاصیتی که قرار است نمایش داده شود، حرف pipe یا | قرار گرفته و سپس نوع pipe ذکر میشود. به این ترتیب کد محصول، پیش از نمایش، ابتدا به حروف کوچک تبدیل شده و سپس نمایش داده میشود.
از pipes در property binding هم میتوان استفاده کرد:
در اینجا برای مثال عنوان تصویر با حروف بزرگ نمایش داده خواهد شد.
و یا میتوان pipes را به صورت زنجیرهای نیز تعریف کرد:
در اینجا pipe توکار currency سبب نمایش سه حرف اول واحد پولی، با حروف بزرگ میشود. اگر علاقمند بودیم که آنرا با حروف کوچک نمایش دهیم میتوان یک pipe دیگر را در انتهای این زنجیره قید کرد.
بعضی از pipes، پارامتر هم قبول میکنند:
در اینجا هر پارامتر با یک : مشخص میشود. برای مثال pipe واحد پولی، سه پارامتر را دریافت میکند: یک کد دلخواه، نمایش یا عدم نمایش علامت پولی، بجای کد دلخواه و مشخصات ارقام نمایش داده شده. برای مثال 2-1.2، یعنی حداقل یک عدد پیش از اعشار، حداقل دو عدد پس از اعشار و حداکثر دو عدد پس از اعشار باید ذکر شوند (یعنی در نهایت دو رقم اعشار مجاز است).
مبحث ایجاد custom pipes را در قسمت بعدی دنبال خواهیم کرد.
در ادامه برای ویرایش مثال جاری، فایل قالب product-list.component.html را گشوده و سطرهای جدول را به نحو ذیل تغییر دهید:
در اینجا با استفاده از pipes، شماره محصول با حروف کوچک و قیمت آن تا حداکثر دو رقم اعشار، فرمت خواهند شد.
اینبار اگر برنامه را اجرا کنید، یک چنین خروجی را مشاهده خواهید کرد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part4.zip
خلاصهی بحث
data binding سبب سهولت نمایش مقادیر خواص کلاس یک کامپوننت، در قالب آن میشود. در AngularJS 2.0، چهار نوع binding وجود دارند:
interpolation، عبارت رشتهای محاسبه شده را در بین المانهای DOM درج میکند و یا میتواند خاصیت المانی را مقدار دهی نماید.
property binding سبب اتصال مقدار خاصیتی، به یکی از خواص المانی مشخص در DOM میشود.
event binding به رخدادها گوش فرا داده و سبب اجرای متدی در کلاس کامپوننت، در صورت بروز رخداد متناظری میشود.
حالت two-way binding، کار دریافت اطلاعات از کلاس و همچنین بازگشت مقادیر تغییر یافتهی توسط کاربر را به کلاس انجام میدهد.
اطلاعات نمایش داده شدهی توسط binding عموما فرمت مناسبی را ندارد. برای رفع این مشکل از pipes استفاده میشود.
انقیاد به خواص یا 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 استفاده میشود.
با تعدادی از کامپوننتهای Bootstrap در مطلب «نگاهی به اجزای تعاملی Twitter Bootstrap» آشنا شدید. یکی دیگر از این افزونهها، Typeahead نام دارد که در حقیقت نوعی Autocomplete text box است. در ادامه قصد داریم نحوه استفاده از آنرا در ASP.NET MVC بررسی کنیم.
استفادهی استاتیک از افزونه Typeahead
منظور از استفادهی استاتیک، مشخص بودن آرایه عناصر و هچنین درج آن به صورت html encoded در صفحه است. برای این منظور، کنترلر برنامه چنین شکلی را خواهد داشت:
در اینجا یک آرایه با تعداد عناصر مشخص، تبدیل به رشته JSON معادل آن شده و توسط ViewBag.JsonString به View ارسال میشود.
View متناظر با آن به نحو ذیل با مشخص سازی نوع data-provide (تا به کتابخانهی جاوا اسکریپتی همراه bootstrap اعلام کند از چه افزونهای در اینجا قرار است استفاده شود)، منبع داده data-source و حداکثر تعداد آیتم ظاهر شونده data-items، میتواند طراحی شود:
به این ترتیب، یک چنین خروجی در صفحه درج میشود:
همانطور که ملاحظه میکنید دقیقا data-source تهیه شده مطابق نیاز خاص این افزونه، html encoded است. به علاوه هر جایی در htmlAttributes صفحه از under line استفاده شده، در این سمت به صورت خودکار به - ترجمه گردیده است.
اگر هم بخواهیم برای آن یک Html Helper درست کنیم، میتوان به نحو ذیل عمل کرد:
استفاده پویا و Ajax ایی از افزونه Typeahead
اگر بخواهیم data-source را به صورت پویا، هربار از بانک اطلاعاتی دریافت و ارائه دهیم، نیاز به کمی اسکریپت نویسی خواهد بود:
در این حالت، کدهای اکشن متدی که یک عبارت، یا قسمتی از آن را از طریق پارامتر term دریافت و خروجی JSON مناسبی را ارائه میکند، همانند متد GetNames فوق خواهد بود.
سپس در تعاریف View، قسمت data-source مرتبط با TextBox حذف و از طریق فراخوانی مستقیم کدهای افزونه typeahead مقدار دهی میگردد:
در اینجا توسط متد getJSON کتابخانه jQuery، مقدار عبارت وارد شده در TextBox جستجو، به آدرس اکشن متد GetNames ارسال و سپس حاصل به source افزونه typeahead انتساب داده میشود.
استفادهی استاتیک از افزونه 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> }