اشتراکها
با تشکر . یعنی موقعی که توکن ایجاد میشود از claimها هم برای ایجاد آن استفاده میشود؟در اینجا
private async Task<(string AccessToken, IEnumerable<Claim> Claims)> createAccessTokenAsync(User user) { var claims = new List<Claim> { // Unique Id for all Jwt tokes new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString(), ClaimValueTypes.String, _configuration.Value.Issuer), // Issuer new Claim(JwtRegisteredClaimNames.Iss, _configuration.Value.Issuer, ClaimValueTypes.String, _configuration.Value.Issuer), // Issued at new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, _configuration.Value.Issuer), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.String, _configuration.Value.Issuer), new Claim(ClaimTypes.Name, user.Username, ClaimValueTypes.String, _configuration.Value.Issuer), new Claim("DisplayName", user.DisplayName, ClaimValueTypes.String, _configuration.Value.Issuer), // to invalidate the cookie new Claim(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String, _configuration.Value.Issuer), // custom data new Claim(ClaimTypes.UserData, user.Id.ToString(), ClaimValueTypes.String, _configuration.Value.Issuer) }; // add userclaims for permission var clmsuser= await _rolesService.FindUserClaimesAsync(user.Id); foreach (var cls in clmsuser) { claims.Add(new Claim(cls.ClaimType,cls.ClaimValue,ClaimValueTypes.String,_configuration.Value.Issuer)); } // add roles var roles = await _rolesService.FindUserRolesAsync(user.Id); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role.Name, ClaimValueTypes.String, _configuration.Value.Issuer)); } var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Value.Key)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var now = DateTime.UtcNow; var token = new JwtSecurityToken( issuer: _configuration.Value.Issuer, audience: _configuration.Value.Audience, claims: claims, notBefore: now, expires: now.AddMinutes(_configuration.Value.AccessTokenExpirationMinutes), signingCredentials: creds); return (new JwtSecurityTokenHandler().WriteToken(token), claims); }
و هر تغییر در claimها در سمت کاربر برابر میشود با برگشت توکن دستکاری شده از سمت سرور و مسدود شدن دسترسی؟
مطالب
ASP.NET MVC #13
اعتبار سنجی اطلاعات ورودی در فرمهای ASP.NET MVC
زمانیکه شروع به دریافت اطلاعات از کاربران کردیم، نیاز خواهد بود تا اعتبار اطلاعات ورودی را نیز ارزیابی کنیم. در ASP.NET MVC، به کمک یک سری متادیتا، نحوهی اعتبار سنجی، تعریف شده و سپس فریم ورک بر اساس این ویژگیها، به صورت خودکار اعتبار اطلاعات انتساب داده شده به خواص یک مدل را در سمت کلاینت و همچنین در سمت سرور بررسی مینماید.
این ویژگیها در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند که به صورت پیش فرض در هر پروژه جدید ASP.NET MVC لحاظ میشود.
یک مثال کاربردی
مدل زیر را به پوشه مدلهای یک پروژه جدید خالی ASP.NET MVC اضافه کنید:
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Models
{
public class Customer
{
public int Id { set; get; }
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
public string Name { set; get; }
[Display(Name = "Email address")]
[Required(ErrorMessage = "Email address is required.")]
[RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
ErrorMessage = "Please enter a valid email address.")]
public string Email { set; get; }
[Range(0, 10)]
[Required(ErrorMessage = "Rating is required.")]
public double Rating { set; get; }
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { set; get; }
}
}
سپس کنترلر جدید زیر را نیز به برنامه اضافه نمائید:
using System.Web.Mvc;
using MvcApplication9.Models;
namespace MvcApplication9.Controllers
{
public class CustomerController : Controller
{
[HttpGet]
public ActionResult Create()
{
var customer = new Customer();
return View(customer);
}
[HttpPost]
public ActionResult Create(Customer customer)
{
if (this.ModelState.IsValid)
{
//todo: save data
return Redirect("/");
}
return View(customer);
}
}
}
بر روی متد Create کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه Create a strongly typed view را انتخاب کرده و مدل را Customer انتخاب کنید. همچنین قالب Scaffolding را نیز بر روی Create قرار دهید.
توضیحات تکمیلی
همانطور که در مدل برنامه ملاحظه مینمائید، به کمک یک سری متادیتا یا اصطلاحا data annotations، تعاریف اعتبار سنجی، به همراه عبارات خطایی که باید به کاربر نمایش داده شوند، مشخص شده است. ویژگی Required مشخص میکند که کاربر مجبور است این فیلد را تکمیل کند. به کمک ویژگی StringLength، حداکثر تعداد حروف قابل قبول مشخص میشود. با استفاده از ویژگی RegularExpression، مقدار وارد شده با الگوی عبارت باقاعده مشخص گردیده، مقایسه شده و در صورت عدم تطابق، پیغام خطایی به کاربر نمایش داده خواهد شد. به کمک ویژگی Range، بازه اطلاعات قابل قبول، مشخص میگردد.
ویژگی دیگری نیز به نام System.Web.Mvc.Compare مهیا است که برای مقایسه بین مقادیر دو خاصیت کاربرد دارد. برای مثال در یک فرم ثبت نام، عموما از کاربر درخواست میشود که کلمه عبورش را دوبار وارد کند. ویژگی Compare در یک چنین مثالی کاربرد خواهد داشت.
در مورد جزئیات کنترلر تعریف شده در قسمت 11 مفصل توضیح داده شد. برای مثال خاصیت this.ModelState.IsValid مشخص میکند که آیا کارmodel binding موفق بوده یا خیر و همچنین اعتبار سنجیهای تعریف شده نیز در اینجا تاثیر داده میشوند. بنابراین بررسی آن پیش از ذخیره سازی اطلاعات ضروری است.
در حالت HttpGet صفحه ورود اطلاعات به کاربر نمایش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دریافت میگردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به کاربر مجددا نمایش داده خواهد شد تا فرم پاک نشود و بتواند آنها را اصلاح کند.
برنامه را اجرا کنید. با مراجعه به مسیر http://localhost/customer/create، صفحه ورود اطلاعات کاربر نمایش داده خواهد شد. در اینجا برای مثال در قسمت ورود اطلاعات آدرس ایمیل، مقدار abc را وارد کنید. بلافاصله خطای اعتبار سنجی عدم اعتبار مقدار ورودی نمایش داده میشود. یعنی فریم ورک، اعتبار سنجی سمت کاربر را نیز به صورت خودکار مهیا کرده است.
اگر علاقمند باشید که صرفا جهت آزمایش، اعتبار سنجی سمت کاربر را غیرفعال کنید، به فایل web.config برنامه مراجعه کرده و تنظیم زیر را تغییر دهید:
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
البته این تنظیم تاثیر سراسری دارد. اگر قصد داشته باشیم که این تنظیم را تنها به یک view خاص اعمال کنیم، میتوان از متد زیر کمک گرفت:
@{ Html.EnableClientValidation(false); }
در این حالت اگر مجددا برنامه را اجرا کرده و اطلاعات نادرستی را وارد کنیم، باز هم همان خطاهای تعریف شده، به کاربر نمایش داده خواهد شد. اما اینبار یکبار رفت و برگشت اجباری به سرور صورت خواهد گرفت، زیرا اعتبار سنجی سمت کاربر (که درون مرورگر و توسط کدهای جاوا اسکریپتی اجرا میشود)، غیرفعال شده است. البته امکان غیرفعال کردن جاوا اسکریپت توسط کاربر نیز وجود دارد. به همین جهت بررسی خودکار سمت سرور، امنیت سیستم را بهبود خواهد بخشید.
نحوه تعریف عناصر مرتبط با اعتبار سنجی در Viewهای برنامه نیز به شکل زیر است:
<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>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Customer</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
همانطور که ملاحظه میکنید به صورت پیش فرض از jQuery validator در سمت کلاینت استفاده شده است. فایل jquery.validate.unobtrusive متعلق به تیم ASP.NET MVC است و کار آن وفق دادن سیستم موجود، با jQuery validator میباشد (validation adapter). در نگارشهای قبلی، از کتابخانههای اعتبار سنجی مایکروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان کتابخانه برگزیده مطرح است.
Unobtrusive همچنین در اینجا به معنای مجزا سازی کدهای جاوا اسکریپتی، از سورس HTML صفحه و استفاده از ویژگیهای data-* مرتبط با HTML5 برای معرفی اطلاعات مورد نیاز اعتبار سنجی است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />
اگر خواستید این مساله را بررسی کنید، فایل web.config قرار گرفته در ریشه اصلی برنامه را باز کنید. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false کرده و بار دیگر برنامه را اجرا کنید. در این حالت کلیه کدهای اعتبار سنجی، به داخل سورس View رندر شده، تزریق میشوند و مجزا از آن نخواهند بود.
نحوهی تعریف این اسکریپتها نیز جالب توجه است. متد Url.Content، یک متد سمت سرور میباشد که در زمان اجرای برنامه، مسیر نسبی وارد شده را بر اساس ساختار سایت اصلاح میکند. حرف ~ بکارگرفته شده، در ASP.NET به معنای ریشه سایت است. بنابراین مسیر نسبی تعریف شده از ریشه سایت شروع و تفسیر میشود.
اگر از این متد استفاده نکنیم، مجبور خواهیم شد که مسیرهای نسبی را به شکل زیر تعریف کنیم:
<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>
در این حالت بسته به محل قرارگیری صفحات و همچنین برنامه در سایت، ممکن است آدرس فوق صحیح باشد یا خیر. اما استفاده از متد Url.Content، کار مسیریابی نهایی را خودکار میکند.
البته اگر به فایل Views/Shared/_Layout.cshtml، مراجعه کنید، تعریف و الحاق کتابخانه اصلی jQuery در آنجا انجام شده است. بنابراین میتوان این دو تعریف دیگر مرتبط با اعتبار سنجی را به آن فایل هم منتقل کرد تا همهجا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهای اعتبار سنجی مدل که به صورت دستی اضافه شده باشند نمایش داده میشود. این مورد در قسمت 11 توضیح داده شد (چون پارامتر آن true وارد شده، فقط خطاهای سطح مدل را نمایش میدهد).
متد Html.ValidationMessageFor، با توجه به متادیتای یک خاصیت و همچنین استثناهای صادر شده حین model binding خطایی را به کاربر نمایش خواهد داد.
اعتبار سنجی سفارشی
ویژگیهای اعتبار سنجی از پیش تعریف شده، پر کاربردترینها هستند؛ اما کافی نیستند. برای مثال در مدل فوق، StartDate نباید کمتر از سال 2000 وارد شود و همچنین در آینده هم نباید باشد. این موارد اعتبار سنجی سفارشی را چگونه باید با فریم ورک، یکپارچه کرد؟
حداقل دو روش برای حل این مساله وجود دارد:
الف) نوشتن یک ویژگی اعتبار سنجی سفارشی
ب) پیاده سازی اینترفیس IValidatableObject
تعریف یک ویژگی اعتبار سنجی سفارشی
using System;
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute
{
public int MinYear { set; get; }
public override bool IsValid(object value)
{
if (value == null) return false;
var date = (DateTime)value;
if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
return false;
return true;
}
}
}
برای نوشتن یک ویژگی اعتبار سنجی سفارشی، با ارث بری از کلاس ValidationAttribute شروع میکنیم. سپس باید متد IsValid آنرا تحریف کنیم. اگر این متد false برگرداند به معنای شکست اعتبار سنجی میباشد.
در ادامه برای بکارگیری آن خواهیم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }
اکنون مجددا برنامه را اجرا نمائید. اگر تاریخ غیرمعتبری وارد شود، اعتبار سنجی سمت سرور رخ داده و سپس نتیجه به کاربر نمایش داده میشود.
اعتبار سنجی سفارشی به کمک پیاده سازی اینترفیس IValidatableObject
یک سؤال: اگر اعتبار سنجی ما پیچیدهتر باشد چطور؟ مثلا نیاز باشد مقادیر دریافتی چندین خاصیت با هم مقایسه شده و سپس بر این اساس تصمیم گیری شود. برای حل این مشکل میتوان از اینترفیس IValidatableObject کمک گرفت. در این حالت مدل تعریف شده باید اینترفیس یاد شده را پیاده سازی نماید. برای مثال:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MvcApplication9.CustomValidators;
namespace MvcApplication9.Models
{
public class Customer : IValidatableObject
{
//... same as before
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var fields = new[] { "StartDate" };
if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
yield return new ValidationResult("Please enter a valid date.", fields);
if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
yield return new ValidationResult("Accepted date should be greater than 2003", fields);
}
}
}
در اینجا در متد Validate، فرصت خواهیم داشت تا به مقادیر کلیه خواص تعریف شده در مدل دسترسی پیدا کرده و بر این اساس اعتبار سنجی بهتری را انجام دهیم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، کافی است توسط yield return new ValidationResult، یک پیغام را به همراه فیلدهایی که باید این پیغام را نمایش دهند، بازگردانیم.
به این نوع مدلها، self validating models هم گفته میشود.
یک نکته:
از MVC3 به بعد، حین کار با ValidationAttribute، امکان تحریف متد IsValid به همراه پارامتری از نوع ValidationContext نیز وجود دارد. به این ترتیب میتوان به اطلاعات سایر خواص نیز دست یافت. البته در این حالت نیاز به استفاده از Reflection خواهد بود و پیاده سازی IValidatableObject، طبیعیتر به نظر میرسد:
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var info = validationContext.ObjectType.GetProperty("Rating");
//...
return ValidationResult.Success;
}
فعال سازی سمت کلاینت اعتبار سنجیهای سفارشی
اعتبار سنجیهای سفارشی تولید شده تا به اینجا، تنها سمت سرور است که فعال میشوند. به عبارتی باید یکبار اطلاعات به سرور ارسال شده و در بازگشت، نتیجه عملیات به کاربر نمایش داده خواهد شد. اما ویژگیهای توکاری مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت کاربر هم فعال هستند و اگر جاوا اسکریپت در مرورگر کاربر غیرفعال نشده باشد، نیازی به ارسال اطلاعات یک فرم به سرور جهت اعتبار سنجی اولیه، نخواهد بود.
در اینجا باید سه مرحله برای پیاده سازی اعتبار سنجی سمت کلاینت طی شود:
الف) ویژگی سفارشی اعتبار سنجی تعریف شده باید اینترفیس IClientValidatable را پیاده سازی کند.
ب) سپس باید متد jQuery validation متناظر را پیاده سازی کرد.
ج) و همچنین مانند تیم ASP.NET MVC، باید unobtrusive adapter خود را نیز پیاده سازی کنیم. به این ترتیب متادیتای ASP.NET MVC به فرمتی که افزونه jQuery validator آنرا درک میکند، وفق داده خواهد شد.
در ادامه، تکمیل کلاس سفارشی MyDateValidator را ادامه خواهیم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;
namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute, IClientValidatable
{
// ... same as before
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "mydatevalidator",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
yield return rule;
}
}
}
در اینجا نحوه پیاده سازی اینترفیس IClientValidatable را ملاحظه مینمائید. ValidationType، نام متدی خواهد بود که در سمت کلاینت، کار بررسی اعتبار دادهها را به عهده خواهد گرفت.
سپس برای مثال یک فایل جدید به نام customvaildation.js به پوشه اسکریپتهای برنامه با محتوای زیر اضافه خواهیم کرد:
/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />
jQuery.validator.addMethod("mydatevalidator",
function (value, element, param) {
return Date.parse(value) < new Date();
});
jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");
توسط referenceهایی که مشاهده میکنید، intellisense جیکوئری در VS.NET فعال میشود.
سپس به کمک متد jQuery.validator.addMethod، همان مقدار ValidationType پیشین را معرفی و در ادامه بر اساس مقدار value دریافتی، تصمیم گیری خواهیم کرد. اگر خروجی false باشد، به معنای شکست اعتبار سنجی است.
همچنین توسط متد jQuery.validator.unobtrusive.adapters.addBool، این متد جدید را به مجموعه وفق دهندهها اضافه میکنیم.
و در آخر این فایل جدید باید به View مورد نظر یا فایل master page سیستم اضافه شود:
<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>
تغییر رنگ و ظاهر پیغامهای اعتبار سنجی
اگر از رنگ پیش فرض قرمز پیغامهای اعتبار سنجی خرسند نیستید، باید اندکی CSS سایت را ویرایش کرد که شامل اعمال تغییرات به موارد ذیل خواهد شد:
1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid
نحوه جدا سازی تعاریف متادیتا از کلاسهای مدل برنامه
فرض کنید مدلهای برنامه شما به کمک یک code generator تولید میشوند. در این حالت هرگونه ویژگی اضافی تعریف شده در این کلاسها پس از تولید مجدد کدها از دست خواهند رفت. به همین منظور امکان تعریف مجزای متادیتاها نیز پیش بینی شده است:
[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
class CustomerMetadata
{
}
}
public partial class Customer : IValidatableObject
{
حالت کلی روش انجام آن هم به شکلی است که ملاحظه میکنید. کلاس اصلی، به صورت partial معرفی خواهد شد. سپس کلاس partial دیگری نیز به همین نام که در برگیرنده یک کلاس داخلی دیگر برای تعاریف متادیتا است، به پروژه اضافه میگردد. به کمک ویژگی MetadataType، کلاسی که قرار است ویژگیهای خواص از آن خوانده شود، معرفی میگردد. موارد عنوان شده، شکل کلی این پیاده سازی است. برای نمونه اگر با WCF RIA Services کار کرده باشید، از این روش زیاد استفاده میشود. کلاس خصوصی تو در توی تعریف شده صرفا وظیفه ارائه متادیتاهای تعریف شده را به فریم ورک خواهد داشت و هیچ کاربرد دیگری ندارد.
در ادامه کلیه خواص کلاس Customer به همراه متادیتای آنها باید به کلاس CustomerMetadata منتقل شوند. اکنون میتوان تمام متادیتای کلاس اصلی Customer را حذف کرد.
اعتبار سنجی از راه دور (remote validation)
فرض کنید شخصی مشغول به پر کردن فرم ثبت نام، در سایت شما است. پس از اینکه نام کاربری دلخواه خود را وارد کرد و مثلا به فیلد ورود کلمه عبور رسید، در همین حال و بدون ارسال کل صفحه به سرور، به او پیغام دهیم که نام کاربری وارد شده، هم اکنون توسط شخص دیگری در حال استفاده است. این مکانیزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و یک درخواست Ajaxایی خودکار را به سرور ارسال خواهد کرد و نتیجه نهایی را به کاربر نمایش میدهد؛ کارهایی که به سادگی توسط کدهای جاوا اسکریپتی قابل مدیریت نیستند و نیاز به تعامل با سرور، در این بین وجود دارد. پیاده سازی آن هم به نحو زیر است:
برای مثال خاصیت Name را در مدل برنامه به نحو زیر تغییر دهید:
[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
controller: "Customer",
AdditionalFields = "Email",
HttpMethod = "POST",
ErrorMessage = "Username is not available.")]
public string Name { set; get; }
سپس متد زیر را نیز به کنترلر Customer اضافه کنید:
[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
if (name.ToLowerInvariant() == "vahid") return Json(false);
if (email.ToLowerInvariant() == "name@site.com") return Json(false);
//...
return Json(true);
}
توضیحات:
توسط ویژگی System.Web.Mvc.Remote، نام کنترلر و متدی که در آن قرار است به صورت خودکار توسط jQuery Ajax فراخوانی شود، مشخص خواهند شد. همچنین اگر نیاز بود فیلدهای دیگری نیز به این متد کنترلر ارسال شوند، میتوان آنها را توسط خاصیت AdditionalFields، مشخص کرد.
سپس در کدهای کنترلر مشخص شده، متدی با پارامترهای خاصیت مورد نظر و فیلدهای اضافی دیگر، تعریف میشود. در اینجا فرصت خواهیم داشت تا برای مثال پس از بررسی بانک اطلاعاتی، خروجی Json ایی را بازگردانیم. return Json false به معنای شکست اعتبار سنجی است.
توسط ویژگی OutputCache، از کش شدن نتیجه درخواستهای Ajaxایی جلوگیری کردهایم. همچنین نوع درخواست هم جهت امنیت بیشتر، به HttpPost محدود شده است.
تمام کاری که باید انجام شود همین مقدار است و مابقی مسایل مرتبط با اعمال و پیاده سازی آن خودکار است.
استفاده از مکانیزم اعتبار سنجی مبتنی برمتادیتا در خارج از ASP.Net MVC
مباحثی را که در این قسمت ملاحظه نمودید، منحصر به ASP.NET MVC نیستند. برای نمونه توسط متد الحاقی زیر نیز میتوان یک مدل را مثلا در یک برنامه کنسول هم اعتبار سنجی کرد. بدیهی است در این حالت نیاز خواهد بود تا ارجاعی را به اسمبلی System.ComponentModel.DataAnnotations، به برنامه اضافه کنیم و تمام عملیات هم دستی است و فریم ورک ویژهای هم وجود ندارد تا یک سری از کارها را به صورت خودکار انجام دهد.
using System.ComponentModel.DataAnnotations;
namespace MvcApplication9.Helper
{
public static class ValidationHelper
{
public static bool TryValidateObject(this object instance)
{
return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
}
}
}
نظرات اشتراکها
تحریم شکن بگذر!
در کل بنظر من وب سایت جالبی نیست وب سایت دولوپر اندروید
وارد کردم در قسمت مخصوص سایت هایی که پشتیبانی میکنه برای گذر از تحریم پیغام : بررسی و سپس پشتیبانی خواهد شد. متشکریم ! را نشان میدهد.
61- :any-link
در مثال فوق، متن Link 1 به رنگ قرمز نمایش مییابد.
62- :local-link
63- :local-link(n)
64- :active-drop-target
تمامی تگهایی را انتخاب میکند که میتوانند نقش لینک را در صفحه ایفا کنند. در واقع تگهای a، area و link را انتخاب مینماید که دارای ویژگی href هستند و به عنوان لینک عمل میکنند.
<style> :any-link { color: red; } </style> <a href="page1.htm">Link 1</a>
Selector | نسخه CSS | |||||
No | No | No | No | No | :any-link | 4 |
تمامی لینک هایی را انتخاب میکند که با آدرس صفحهی جاری یکسان میباشند.
<style> :local-link { color: red; } </style> <a href="http://www.example.com/article/">Link 1</a> <a href="http://www.example.com/article">Link 2</a> <a href="http://www.example.com/article/">Link 3</a> <a href="http://www.example.com/article/12">Link 4</a>
با فرض اینکه آدرس صفحهی جاری http://www.example.com/article/ می باشد مثال فوق را بررسی میکنیم. متن Link 1 و Link3 به رنگ قرمز نمایش مییابند، زیرا مشابه آدرس صفحهی جاری میباشند.
Selector | نسخه CSS | |||||
No | No | No | No | No | :local-link | 4 |
تمامی لینک هایی را انتخاب میکند که آدرس آنها با آدرس صفحهی جاری یکسان بوده و n بخش از آدرس آنها با بخشهای آدرس جاری یکسان باشند. در واقع n، تعداد بخش هایی از آدرس لینک را مشخص میکند که با بخشهای آدرس صفحهی جاری مطابقت داشته باشند. در مطابقت آدرس، از scheme، username، password، port، query string و fragment صرف نظر میشود تا یکسان بودن آدرس را تشخیص دهد. سپس تشخیص میدهد کدام بخشهای آدرس با هم یکسان میباشند. جهت اطلاع باید عرض کنم که شکل کامل یک آدرس به صورت scheme:[//[user:password@]host[:port]][/]path[?querystring][#fragment] میباشد.
<style> :local-link { color: red; } :local-link(0) { text-decoration: none; } :local-link(1) { text-decoration: overline; } :local-link(2) { background: blue; } </style> <a href="http://www.example.com">Link 1</a> <a href="http://www.example.com/2016">Link 2</a> <a href="http://www.example.com/2016/01">Link 3</a> <a href="http://www.example.com/2016/01/">Link 4</a> <a href="http://www.example.com/2016/01/02">Link 5</a> <a href="https://www.example.com/2016/01/">Link 6</a> <a href="http://example.com/2016/01">Link 7</a>
با فرض اینکه آدرس صفحهی جاری http://www.example.com/2016/01/ می باشد مثال فوق را بررسی میکنیم. Link 1 تحت تاثیر Selector دوم، بدون زیرخط نمایش مییابد. Link 2 تحت تاثیر Selector دوم و سوم، بدون زیرخط و با روخط نمایش مییابد. Link 3، Link 5 و Link 6 تحت تاثیر Selector دوم و سوم و چهارم، بدون زیرخط، با روخط و پس زمینهی آبی نمایش مییابند. Link 6 به دلیل استفاده از https نمیتواند تحت تاثیر Selector اول قرار بگیرد. Link 4 تحت تاثیر Selector اول و دوم و سوم و چهارم، به رنگ قرمز، بدون زیرخط، با روخط و پس زمینهی آبی نمایش مییابد. Link 7 هم بدون تاثیر هیچ Selector ی بر روی آن، بدون قالب باقی میماند، زیرا قسمت host لینک با آدرس مطابقت ندارد.
Selector | نسخه CSS | |||||
No | No | No | No | No | :local-link(n) | 4 |
تگی را انتخاب میکند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است. به عبارت دیگر، تگی را با عمل drag کشیده ایم و بر روی یک تگ مقصد قرار داده ایم، ولی هنوز عمل drop یا رها کردن صورت نگرفته است.
Selector | نسخه CSS | |||||
No | No | No | No | No | :active-drop-target | 4 |
65- :valid-drop-target
تگی را انتخاب میکند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است و برای عمل drop معتبر میباشد. به عبارت دیگر، تگ مقصد امکان پذیرش یک تگ را با عمل Drag & Drop دارد.
Selector | نسخه CSS | |||||
No | No | No | No | No | :valid-drop-target | 4 |
66- :invalid-drop-target
در مثال فوق، با Drag نمودن تصویر بر روی تگ div، تحت تاثیر Selector اول، رنگ پس زمینهی div آبی میشود. اگر امکان پذیرش img توسط تگ div وجود داشته باشد، کادری سبز رنگ دور تگ div نمایش مییابد در غیر اینصورت کادر قرمز رنگ دور آن نمایش خواهد یافت.
تگی را انتخاب میکند که در حال حاضر به عنوان مقصد یک تگ drag (کشیده) شده در نظر گرفته شده است و برای عمل drop معتبر نمیباشد. به عبارت دیگر، تگ مقصد امکان پذیرش یک تگ را با عمل Drag & Drop ندارد.
Selector | نسخه CSS | |||||
No | No | No | No | No | :invalid-drop-target | 4 |
<style> :active-drop-target { background: blue; } :valid-drop-target { border: 1px solid green; } :invalid-drop-target { border: 1px solid red; } .container { width: 200px; height: 200px; } </style> <div class="container"></div> <img src="image.jpg" draggable="true"/>
67- :current
تگی را انتخاب میکند که در حال حاضر در حال نمایش یا ارائه میباشد. این Selector در زمان پردازش صوت، تصویر، نمایش زیر نویس و غیره در تگ canvas مورد استفاده قرار میگیرد.
Selector | نسخه CSS | |||||
No | No | No | No | No | :current | 4 |
68- :past
تگی را انتخاب میکند که قبل از :current نمایش یافته است.
Selector | نسخه CSS | |||||
No | No | No | No | No | :past | 4 |
69- :future
هر سه Selector فوق میتوانند با دریافت آرگومان به صورت مجموعه ای از Selector ها، تگهای خاصی را انتخاب کنند یا نتیجهی انتخاب را محدود نمایند. اگر بدون آرگومان به کار روند، تک مورد نظر را بدون در نظر گرفتن محدویت انتخاب مینمایند.
در مثال فوق، تگهای div یا p که در حال حاضر در حال نمایش میباشند، با پس زمینهی سبز، تگی که قبل از :current نمایش یافته است با پس زمینهی قرمز و تگی که بعد از :current نمایش خواهد یافت با پس زمینهی زرد نمایش مییابد.
تگی را انتخاب میکند که بعد از :current نمایش یافته است.
Selector | نسخه CSS | |||||
No | No | No | No | No | :future | 4 |
<style> :past(div,p) { background: red; } :current(div,p) { background: green; } :future(div,p) { background: yellow; } </style>
70- :placeholder-shown
در مثال فوق، اگر در هر کدام از تگهای input، هیچ متنی وارد نشده باشد، متنهای Enter Username و Enter Password به رنگ طوسی نمایش مییابند.
تمامی تگهای input و textarea را انتخاب میکند که در حال نمایش یا حاوی placeholder میباشند و هنوز متنی در آنها وارد نشده است. در حال حاضر به صورت آزمایشی و با vendor prefix قابل استفاده است.
<style> :placeholder-shown { color: gray; } </style> <input placeholder="Enter Username"/> <input placeholder="Enter Password"/>
Selector | نسخه CSS | |||||
-webkit- | -wekit- | -ms- | -moz- | -webkit- | :placeholder-shown | 4 |
| | | | | | |
| | | | | | |
در مقاله قبل توضیح دادیم که وظیفه httphandler رندر و پردازش خروجی یک درخواست هست؛ حالا در این مقاله قصد داریم که مفهوم httphandler را بیشتر بررسی کنیم.
HttpHandler
برای تهیهی یک httphandler، باید کلاسی را بر اساس اینترفیس IHttpHandler پیاده سازی کنیم و بعدا آن را در web.config برنامه معرفی کنیم. برای پیاده سازی این اینترفیس، به یک متد به اسم ProcessRequest با یک پارامتر از نوع HttpContext و یک پراپرتی به اسم IsReusable نیاز داریم که مقدار برگشتی این پراپرتی را false بگذارید؛ بعدا خواهم گفت چرا اینکار را میکنیم. نحوهی پیادهسازی یک httphandler به شکل زیر است:
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { } public bool IsReusable { get { return false; } } }
public class MyHttpHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; HttpRequest request = context.Request; response.Write("Every Page has a some text like this"); } public bool IsReusable { get { return false; } } }
<system.web> <httpHandlers> <add verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web>
<configuration> <system.web> <httpHandlers> <add name="myhttphandler" verb="*" path="*.aspx" type="MyHttpHandler"/> </httpHandlers> </system.web> </configuration>
خروجی نهایی باید تنها این متن باشد: Every Page has a some text like this
گزینه Type که نام کلاس میباشد و اگر کلاس داخل یک فضای نام قرار گرفته باشد، باید اینطور نوشت : namespace.ClassName |
گزینه verb شامل مقادیری چون Get,Post,Head,Putو Delete میباشد و httphandler را فقط برای این نوع درخواستها اجرا میکند و در صورتیکه بخواهید چندتا از آنها را استفاده کنید، با , از هم جدا میشوند. مثلا Get,post و درصورتیکه همهی گزینهها را بخواهید علامت * را میتوان استفاده کرد. |
گزینهی path این امکان را به شما میدهد که مسیر و نوع فایلهایی را که قصد دارید روی آنها فقط اجرا شود، مشخص کنید و ما در قطعه کد بالا گفتهایم که تنها روی فایلهایی با پسوند aspx اجرا شود و چون مسیری هم ذکر نکردیم برای همهی مسیرها قابل اجراست. یکی از مزیتهای دادن پسوند این است که میتوانید پسوندهای اختصاصی داشته باشید. مثلا پسوند RSS برای فیدهای وب سایتتان. بسیاری از برنامه نویسان به جای استفاده از صفحات aspx از ashx استفاده میکنند که به مراتب سبکتر از aspx هست و شامل بخش ui نمیشود و نتیجه خروجی آن بر اساس کدی که مینویسید مشخص میشود که میتواند صفحه متنی یا عکس یا xml یا ... باشد. در اینجا در مورد ساخت صفحات ashx توضیح داده شده است. |
IHttpHandlerFactory
کار این اینترفیس پیاده سازی یک کلاس است که خروجی آن یک کلاس از نوع IHttpHandler هست. اگر دقت کنید در مثالهای قبلی ما برای معرفی یک هندلر در وب کانفیگ یک سری path را به آن میدادیم و برای نمونه aspx.* را معرفی میکردیم؛ یعنی این هندلر را بر روی همهی فایلهای aspx اجرا کن و اگر دو یا چند هندلر در وب کانفیگ معرفی کنیم و برای همه مسیر aspx را قرار بدهیم، یعنی همه این هندلرها باید روی صفحات aspx اجرا گردند ولی در httphandlerfactory، ما چند هندلر داریم و میخواهیم فقط یکی از آنها بر روی صفحات aspx انجام بگیرد، پس ما یک هندلرفکتوری را برای صفحات aspx معرفی میکنیم و در حین اجرا تصمیم میگیریم که کدام هندلر را ارسال کنیم.
اجازه بدهید نوشتن این نوع کلاس را آغاز کنیم،ابتدا دو هندلر به نامهای httphandler1 و httphandler2 مینویسیم :
public class MyHttpHandler1 :IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler1"); } public bool IsReusable { get { return false; } } } public class MyHttpHandler2 : IHttpHandler { public void ProcessRequest(HttpContext context) { HttpResponse response = context.Response; response.Write("this is httphandler2"); } public bool IsReusable { get { return false; } } }
public class MyFactory : IHttpHandlerFactory { public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { } public void ReleaseHandler(IHttpHandler handler) { } }
Context | یک شی از کلاس httpcontext که دسترسی ما را برای اشیاء سروری چون response,request,session و... فراهم میکند. |
RequestType | مشخص میکند که درخواست صفحه به چه صورتی است. این گزینه برای مواردی است که verb بیش از یک مورد را حمایت میکند. برای مثال دوست دارید یک هندلر را برای درخواستهای Get ارسال کنید و هندلر دیگر را برای درخواستهای نوع Post |
URL | مسیر مجازی virtual Path صفحه صدا زده شده |
PathTranslated | مسیر فیزیکی صفحه درخواست کننده را ارسال میکند. |
public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTrasnlated) { string handlername = "MyHttpHandler1"; if(url.Substring(url.LastIndexOf("/")+1).StartsWith("t")) { handlername = "MyHttpHandler2"; } try { return (IHttpHandler) Activator.CreateInstance(Type.GetType(handlername)); } catch (Exception e) { throw new HttpException("Error: " + handlername, e); } } public void ReleaseHandler(IHttpHandler handler) { } }
نحوهی تعریف factory در وب کانفیگ مانند قبل است و فقط باید در Type به جای نام هندلر نام فکتوری را نوشت. برنامه را اجرا کنید تا نتیجه آن را ببینیم:
تصویر زیر نتیجه صدا زده شدن فایل default.aspx است:
تصویر زیر نتیجه صدا زده شدن فایل Tours_List.aspx است:
AsyncHttpHandlers
برای اینکه کار این اینترفیس را درک کنید بهتر هست اینجا را مطالعه کنید. در اینجا به خوبی تفاوت متدهای همزمان و غیرهمزمان توضیح داده شده است.
متن زیر خلاصهترین و بهترین توضیح برای این پرسش است، چرا غیرهمزمان؟
موقعی که اینترفیس IHttpAsyncHandler را ارث بری کنید (این اینترفیس نیز از IHttpHandler ارث بری کرده است و دو متد اضافهتر دارد)، باید دو متد دیگر را نیز پیاده سازی کنید:در اعمالی که disk I/O و یا network I/O دارند، پردازش موازی و اعمال async به شدت مقیاس پذیری سیستم را بالا میبرند. به این ترتیب worker thread جاری (که تعداد آنها محدود است)، سریعتر آزاد شده و به worker pool بازگشت داده میشود تا بتواند به یک درخواست دیگر رسیده سرویس دهد. در این حالت میتوان با منابع کمتری، درخواستهای بیشتری را پردازش کرد.
public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { } public void EndProcessRequest(IAsyncResult result) { }
پراپرتی ISResuable هم موقعی که true برگشت بدهد، باعث میشود pooling فعال شده و این هندلر در حافظه باقی بماند و تمامی درخواستها از طریق همین یک نمونه اجرا شوند.
به زبان سادهتر، این پراپرتی میگوید اگر چندین درخواست از طرف کلاینتها برسد، توسط یک نمونه یا instance از هندلر پردازش خواهند شد؛ چون به طور پیش فرض موقعی که تمام درخواستهای از pipeline بگذرند، هندلرها توسط httpapplication در یک لیست بازیافت قرار گرفته و همهی آنها با null مقداردهی میشوند تا از حافظه پاک شوند ولی اگر این پراپرتی true برگرداند، هندلر مربوطه نال نشده و برای پاسخگویی به درخواستهای بعدی در حافظه خواهد ماند.
مهمترین مزیت این گزینه، این میباشد که کاآیی سیستم را بالا میبرد و اشیا کمتری به GC پاس میشوند. ولی یک عیب هم دارد که این تردهایی که ایجاد میکند، امنیت کمتری دارند و باید توسط برنامه نویس این امنیت بالاتر رود. این پراپرتی را در مواقعی که با هندلرهای همزمان کار میکنید برابر با false بگذارید چون این گزینه بیشتر بر روی هندلرهای غیرهمزمان اثر دارد و هم اینکه بعضیها توصیه میکنند که false بگذارید چون GC مدیریت خوبی در مورد هندلرها دارد و هم این که ارزش یافتن باگ در کد را ندارد.
بر میگردیم سراغ کد نویسی هندلر غیر همزمان. در آخرین قطعه کد نوشته شده، ما دو متد دیگر را پیاده سازی کردیم که یکی از آنها BeginProcessRequest است و خروجی آن کلاسی است که از اینترفیس IAsyncResult ارث بری کرده است. پس یک کلاس با ارث بری از این اینترفیس مینویسیم و در این کلاس نیاز است که 4 پراپرتی را پیاده سازی کنیم که این کلاس به شکل زیر در خواهد آمد:
public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } }
خب اجازه بدهید یک تابع سازنده به آن برای مقداردهی اولیه این متغیرهای خصوصی داشته باشیم:
public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; }
همانطور که میبینید موارد موجود در متد BeginProcessRequest را تحویل میگیریم تا اطلاعات درخواستی مربوطه را داشته باشیم و مقدار _Completed را هم برابر با false قرار میدهیم. سپس نوبت این میرسد که ما درخواست را در صف pool قرار دهیم. برای همین تکه کد زیر را اضافه میکنیم:
public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); }
private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
نهایتا کل این کلاس را در متد BeginProcessRequest صدا بزنید:
context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch;
public class MyHttpHandler : IHttpAsyncHandler { public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback callback, object obj) { context.Response.Write("<p>Begin IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); AsynchOperation asynch = new AsynchOperation(callback, context, obj); asynch.StartAsyncWork(); return asynch; } public void EndProcessRequest(IAsyncResult result) { } public void ProcessRequest(HttpContext context) { throw new InvalidOperationException(); } public bool IsReusable { get { return false; } } } public class AsynchOperation : IAsyncResult { private bool _completed; private Object _state; private AsyncCallback _callback; private HttpContext _context; bool IAsyncResult.IsCompleted { get { return _completed; } } WaitHandle IAsyncResult.AsyncWaitHandle { get { return null; } } Object IAsyncResult.AsyncState { get { return _state; } } bool IAsyncResult.CompletedSynchronously { get { return false; } } public AsynchOperation(AsyncCallback callback, HttpContext context, Object state) { _callback = callback; _context = context; _state = state; _completed = false; } public void StartAsyncWork() { ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask),null); } private void StartAsyncTask(Object workItemState) { _context.Response.Write("<p>Completion IsThreadPoolThread is " + Thread.CurrentThread.IsThreadPoolThread + "</p>\r\n"); _context.Response.Write("Hello World from Async Handler!"); _completed = true; _callback(this); }
آشنایی با فایل ASHX
در مطالب بالاتر به فایلهای Ashx اشاره کردیم. این فایل به نام Generic Web Handler شناخته میشوند و میتوانید با Add New Item این نوع فایلها را اضافه کنید. این فایل شامل هیچ UI ایی نمیباشد و فقط شامل بخش کد میباشد. برای همین نسبت به aspx سبکتر بوده و شامل یک directive به اسم WebHandler@ است.
مایکروسافت در MSDN نوشته است که httphandlerها در واقع فرآیندهایی هستند (به این فرایندها بیشتر End Point میگویند) که در پاسخ به درخواستهای رسیده شده توسط asp.net application اجرا میشوند و بیشترین درخواست هایی هم که میرسد از نوع صفحات Aspx میباشد و موقعی که کاربری درخواست صفحهی aspx میکند هندلرهای مربوط به page اجرا میشوند.
در متن بالا به خوبی روشن هست که ashx به دلیل نداشتن UI، تعداد کمتری از handlerها را در مسیر Pipeline قرار میدهند و اجرای آنها سریعتر است. غیر از این دو هندلر aspx و ashx، هندلر توکار دیگری چون asmx که مختص وب سرویس هست و axd مربوط به اعمال trace نیز وجود دارند.
در این لینک که در بالاتر هم درج شده بود یک نمونه هندلر برای نمایش تصویر نوشته است. اگر تصاویرتان را بدین صورت اجرا کنید میتوان جلوی درخواستهای رسیده از وب سایتهای دیگر را سد کرد. برای مثال یک نفر مطالب شما را کپی میکند و در داخل وبلاگ یا وب سایتش میگذارد و شما در اینجا درخواستهای رسیده خارج از وب سایت خود را لغو خواهید کرد و تصاویر کپی شده نمایش داده نخواهند شد.
مطالب دورهها
اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
قصد داریم سیستمی را طراحی کنیم که افزونههای خود را در زمان اجرا از مسیری خاص خوانده و سپس وهلههای آنهارا جهت استفاده در دسترس قرار دهد. برنامهای که در اینجا مورد بررسی قرار میگیرد، یک برنامهی WinForms ساده است؛ به نام WinFormsWithPluginSupport. اما اصول کلی مطرح شده، در تمام فناوریهای دیگر دات نتی نیز کاربرد دارد و یکسان است.
تهیه قرارداد
یک پروژهی Class library به نام PluginsBase را به Solution جاری اضافه کنید. به آن اینترفیس قرار داد پلاگینهای برنامه خود را اضافه نمائید. برای مثال:
هر پلاگین دارای یک نام یا توضیح خاص خود خواهد بود به همراه متدی برای اجرای منطق مرتبط با آن.
تهیه سه پلاگین جدید
به Solution جاری سه پروژهی مجزای Class library با نامهای plugin1 تا 3 را اضافه کنید. در ادامه به هر پلاگین، ارجاعی را به اسمبلی PluginsBase، برای دریافت قرارداد پیاده سازی منطق پلاگین، اضافه نمائید. هدف این است که اینترفیس IPlugin، در این اسمبلیها قابل دسترسی شود.
هر پلاگین هم دارای برای مثال کدهایی مانند کد ذیل خواهد بود که در آن صرفا نام آنها به 2 و 3 تنظیم میشود.
کپی خودکار پلاگینها به پوشهی مخصوص آنها
به پروژهی WinFormsWithPluginSupport مراجعه کنید. در پوشهی bin\debug آن یک پوشهی جدید به نام Plugins ایجاد نمائید. بدیهی است هربار که پلاگینهای برنامه تغییر کنند نیاز است اسمبلیهای نهایی آنها را به این پوشه کپی نمائیم. اما راه بهتری نیز وجود دارد. به خواص هر کدام از پروژههای پلاگین مراجعه کرده و برگهی Build events را باز کنید.
در اینجا قسمت post-build event را به نحو ذیل تغییر دهید:
این کار را برای هر سه پلاگین تکرار کنید.
به این ترتیب هربار که پلاگین جاری کامپایل شود، پس از آن به صورت خودکار به پوشهی plugins تعیین شده، کپی میشود و دیگر نیازی به کپی دستی نخواهد بود.
تنظیم فوق، تنها اسمبلی اصلی پروژه را به پوشهی bin\debug\plugins کپی میکند. اگر میخواهید تمام فایلها کپی شوند، از تنظیم ذیل استفاده کنید:
اضافه کردن وابستگیهای اصلی پروژهی WinForms
در ادامه بستهی نیوگت StructureMap را به پروژهی WinForms از طریق دستور ذیل اضافه کنید:
همچنین این پروژه تنها نیاز دارد ارجاع مستقیمی را به اسمبلی PluginsBase ابتدای مطلب داشته باشد. از آن، جهت تنظیمات اولیه یافتن افزونهها استفاده میکنیم.
تعریف محل ثبت پلاگینها
روشهای متفاوتی برای کار با StructureMap وجود دارد. یکی از آنها تعریف کلاسی است مشتق شده از کلاس Registry آن به نحو ذیل:
در اینجا مشخص کردهایم که اسمبلیهای پوشه plugins را که یک سطح پایینتر از پوشهی اجرایی برنامه قرار میگیرند، خوانده و در این بین آنهایی را که پیاده سازی از اینترفیس IPlugin دارند، در دسترس قرار دهد.
یک نکتهی مهم
در قسمت assemblyFilter تعیین کردهایم که اسمبلی تکراری PluginBase بارگذاری نشود. چون این اسمبلی هم اکنون به برنامهی WinForms ارجاع دارد. رعایت این نکته جهت رفع تداخلات آتی بسیار مهم است. همچنین این فایل در پوشهی Plugins نیز نباید حضور داشته باشد وگرنه شاهد بارگذاری افزونهها نخواهید بود.
سپس نیاز به وهله سازی Container آن و معرفی این کلاس PluginsRegistry میباشد:
تنظیمات ابتدایی WinForms برای دسترسی به امکانات StructureMap
به فرم اصلی برنامه مراجعه کرده و به سازندهی آن IContainer را اضافه کنید. از این اینترفیس جهت دسترسی به پلاگینهای برنامه استفاده خواهیم کرد.
اکنون برنامه دیگر کامپایل نخواهد شد؛ چون در فایل Program.cs وهله سازی مستقیمی از FrmMain وجود دارد. این وهله سازی را اکنون به StructureMap محول میکنیم تا مشکل برطرف شود:
زمانیکه از متد IocConfig.Container.GetInstance استفاده میشود، تا هر تعداد سطحی که تعریف شده، سازندههای کلاسهای مرتبط وهله سازی میشوند. در اینجا نیاز است سازندهی کلاس FrmMain وهله سازی شود. چون IContainer اینترفیس اصلی خود StructureMap است، آنرا شناخته و به صورت خودکار وهله سازی میکند. اگر اینترفیس دیگری را ذکر کنید، نیاز است مطابق معمول آنرا در کلاس IocConfig و متد defaultContainer آن معرفی نمائید.
بارگذاری و اجرای افزونهها
دو دکمهی Run و ReLoad را به فرم اصلی برنامه با کدهای ذیل اضافه کنید:
در اینجا توسط متد container.GetAllInstances میتوان به تمامی وهلههای پلاگینهای بارگذاری شده، دسترسی یافت و سپس آنها را اجرا کرد.
همچنین در متد ReLoad نحوهی بارگذاری مجدد این پلاگینها را در صورت نیاز مشاهده میکنید.
اگر برنامه را اجرا کردید و پلاگینی بارگذاری نشد، به دنبال اسمبلیهای تکراری بگردید. برای مثال PluginsBase نباید هم در پوشهی اصلی اجرایی برنامه حضور داشته باشد و هم در پوشهی پلاگینها.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
WinFormsWithPluginSupport.zip
تهیه قرارداد
یک پروژهی Class library به نام PluginsBase را به Solution جاری اضافه کنید. به آن اینترفیس قرار داد پلاگینهای برنامه خود را اضافه نمائید. برای مثال:
namespace PluginsBase { public interface IPlugin { string Name { get; } void Run(); } }
تهیه سه پلاگین جدید
به Solution جاری سه پروژهی مجزای Class library با نامهای plugin1 تا 3 را اضافه کنید. در ادامه به هر پلاگین، ارجاعی را به اسمبلی PluginsBase، برای دریافت قرارداد پیاده سازی منطق پلاگین، اضافه نمائید. هدف این است که اینترفیس IPlugin، در این اسمبلیها قابل دسترسی شود.
هر پلاگین هم دارای برای مثال کدهایی مانند کد ذیل خواهد بود که در آن صرفا نام آنها به 2 و 3 تنظیم میشود.
using PluginsBase; namespace Plugin1 { public class Plugin1Main : IPlugin { public string Name { get { return "Test 1"; } } public void Run() { // todo: ... } } }
کپی خودکار پلاگینها به پوشهی مخصوص آنها
به پروژهی WinFormsWithPluginSupport مراجعه کنید. در پوشهی bin\debug آن یک پوشهی جدید به نام Plugins ایجاد نمائید. بدیهی است هربار که پلاگینهای برنامه تغییر کنند نیاز است اسمبلیهای نهایی آنها را به این پوشه کپی نمائیم. اما راه بهتری نیز وجود دارد. به خواص هر کدام از پروژههای پلاگین مراجعه کرده و برگهی Build events را باز کنید.
در اینجا قسمت post-build event را به نحو ذیل تغییر دهید:
Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)WinFormsWithPluginSupport\bin\debug\Plugins"
به این ترتیب هربار که پلاگین جاری کامپایل شود، پس از آن به صورت خودکار به پوشهی plugins تعیین شده، کپی میشود و دیگر نیازی به کپی دستی نخواهد بود.
تنظیم فوق، تنها اسمبلی اصلی پروژه را به پوشهی bin\debug\plugins کپی میکند. اگر میخواهید تمام فایلها کپی شوند، از تنظیم ذیل استفاده کنید:
Copy "$(ProjectDir)$(OutDir)*.*" "$(SolutionDir)WinFormsWithPluginSupport\bin\debug\Plugins"
اضافه کردن وابستگیهای اصلی پروژهی WinForms
در ادامه بستهی نیوگت StructureMap را به پروژهی WinForms از طریق دستور ذیل اضافه کنید:
PM> install-package structuremap
تعریف محل ثبت پلاگینها
روشهای متفاوتی برای کار با StructureMap وجود دارد. یکی از آنها تعریف کلاسی است مشتق شده از کلاس Registry آن به نحو ذیل:
using System.IO; using System.Windows.Forms; using PluginsBase; using StructureMap.Configuration.DSL; using StructureMap.Graph; namespace WinFormsWithPluginSupport.Core { public class PluginsRegistry : Registry { public PluginsRegistry() { this.Scan(scanner => { scanner.AssembliesFromPath( path: Path.Combine(Application.StartupPath, "plugins"), // یک اسمبلی نباید دوبار بارگذاری شود assemblyFilter: assembly => { return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName); }); scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName); }); } } }
یک نکتهی مهم
در قسمت assemblyFilter تعیین کردهایم که اسمبلی تکراری PluginBase بارگذاری نشود. چون این اسمبلی هم اکنون به برنامهی WinForms ارجاع دارد. رعایت این نکته جهت رفع تداخلات آتی بسیار مهم است. همچنین این فایل در پوشهی Plugins نیز نباید حضور داشته باشد وگرنه شاهد بارگذاری افزونهها نخواهید بود.
سپس نیاز به وهله سازی Container آن و معرفی این کلاس PluginsRegistry میباشد:
using System; using System.Threading; using StructureMap; namespace WinFormsWithPluginSupport { public static class IocConfig { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(x => { x.AddRegistry<PluginsRegistry>(); }); } } }
تنظیمات ابتدایی WinForms برای دسترسی به امکانات StructureMap
به فرم اصلی برنامه مراجعه کرده و به سازندهی آن IContainer را اضافه کنید. از این اینترفیس جهت دسترسی به پلاگینهای برنامه استفاده خواهیم کرد.
using System.Windows.Forms; using StructureMap; namespace WinFormsWithPluginSupport { public partial class FrmMain : Form { private readonly IContainer _container; public FrmMain(IContainer container) { _container = container; InitializeComponent(); } } }
using System; using System.Windows.Forms; namespace WinFormsWithPluginSupport { static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(IocConfig.Container.GetInstance<FrmMain>()); } } }
بارگذاری و اجرای افزونهها
دو دکمهی Run و ReLoad را به فرم اصلی برنامه با کدهای ذیل اضافه کنید:
using System.Linq; using System.Windows.Forms; using PluginsBase; using StructureMap; using WinFormsWithPluginSupport.Core; namespace WinFormsWithPluginSupport { public partial class FrmMain : Form { private readonly IContainer _container; public FrmMain(IContainer container) { _container = container; InitializeComponent(); } private void BtnRun_Click(object sender, System.EventArgs e) { var plugins = _container.GetAllInstances<IPlugin>().ToList(); foreach (var plugin in plugins) { plugin.Run(); } } private void BtnReload_Click(object sender, System.EventArgs e) { _container.EjectAllInstancesOf<IPlugin>(); _container.Configure(x => x.AddRegistry<PluginsRegistry>() ); } } }
همچنین در متد ReLoad نحوهی بارگذاری مجدد این پلاگینها را در صورت نیاز مشاهده میکنید.
اگر برنامه را اجرا کردید و پلاگینی بارگذاری نشد، به دنبال اسمبلیهای تکراری بگردید. برای مثال PluginsBase نباید هم در پوشهی اصلی اجرایی برنامه حضور داشته باشد و هم در پوشهی پلاگینها.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
WinFormsWithPluginSupport.zip
ارسال تصویر در سایتها، انجمنها و امثال آن همیشه مشکل زا است؛ خصوصا نمایش تصاویری در قطع یک تابلوی دیواری در یک کادر کوچک.
راه ساده و دم دستی زیر، منهای استفاده از اسکریپتهایی که اندازه تصاویر را تشخیص داده و بر این اساس خودشان آنها را بند انگشتی نمایش میدهند، هم برای حل این مشکل وجود دارد:
فرض کنید مطلبی که قرار است ارسال شود در یک DIV نمایش داده میشود و کلاس آن مثلا post-body است.
اولین کاری که میشود انجام داد این است:
در CSS سایت قسمت مرتبط با post-body تعریف زیر را اضافه میکنیم تا اضافات عکس که از کادر بیرون زده، خودبخود توسط مرورگر نمایش داده نشود:
overflow: hidden;
سپس در همین DIV که کلاس آن post-body است، با استفاده از jQuery به دنبال تصاویر گشته و موارد یافت شده را داخل یک لینک پویا قرار میدهیم (استفاده از متد wrap برای این محصور سازی). این لینک هم همان آدرس اصلی تصویر است. به این ترتیب کاربر با کلیک بر روی آن تصویر میتواند نتیجه را در صفحهای دیگر مشاهده کند:
$(document).ready(function(){
$('.post-body img').each(function(){
var $img = $(this);
var src = $img.attr("src");
$img.attr({border:"0"}).wrap('<a target="_blank" alt="Click here to enlarge (opens new window)" title="Click here to enlarge (opens new window)" href="'+src+'" />');
});
});
در سری بررسی اعتبارسنجی و احراز هویت کاربران در React، برای انتقال دادههای کاربر وارد شدهی به سیستم، از روش انتقال props، از بالاترین کامپوننت موجود در component tree، به پایینترین کامپوننت آن، به این نحو فرضی استفاده کردیم:
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحهای خاص از طریق ویژگیهای props ارسال میشود:
سپس این کامپوننت Page، کامپوننت PageLayout را رندر میکند که آن نیز باید به اطلاعات کاربر دسترسی داشته باشد. بنابراین شیء user را مجددا به این کامپوننت از طریق props ارسال میکنیم:
بعد همین کامپوننت PageLayout، کامپوننت NavBar را رندر میکند که آن نیز باید بداند کاربر وارد شدهی به سیستم کیست؟ به همین جهت یکبار دیگر از طریق props، اطلاعات کاربر را به کامپوننت بعدی موجود در درخت کامپوننتها انتقال میدهیم:
و همینطور الی آخر. به این روش props drilling گفته میشود و ... الگوی مذمومی است. در دنیای واقعی، اطلاعات کاربر و یا خصوصا تنظیمات برنامه مانند آدرس REST API endpoints استفاده شدهی در آن، باید بین بسیاری از کامپوننتها به اشتراک گذاشته شود و عموما سطوح به اشتراک گذاری آن، بسیار عمیقتر است از سطوحی که در این مثال ساده عنوان شدند. از زمان ارائهی React 16.3.0، راه حل بهتری برای مدیریت اینگونه مسایل با ارائهی React Context ارائه شدهاست که آنرا در ادامه در دو حالت کامپوننتهای کلاسی و همچنین تابعی، بررسی خواهیم کرد.
ایجاد شیء Context در برنامههای React
React Context، راه حلی است جهت به اشتراک گذاری دادهها، در بین انواع و اقسام کامپوننتهای یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونهی از آن، ابتدا پوشهی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد میکنیم:
متد React.createContext، یک شیء Context را بازگشت میدهد. این شیء، دو کامپوننت مهم Provider و Consumer را به همراه دارد که امکان اشتراک به دادههای مرتبط با آنرا میسر میکنند. زمانیکه React کامپوننتی را رندر میکند که مشترک یک شیء Context است، این کامپوننت، امکان خواندن اطلاعات شیء Context را از نزدیکترین کامپوننتی در درخت کامپوننتها که یک Provider را برای آن ارائه دادهاست، خواهد داشت.
تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی
تا اینجا یک شیء Context را به همراه اجزای export شدهی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
در این کامپوننت کلاسی (و یا تابعی، نحوهی تعریف UserProvider در هر دو یکی است)، خاصیت user، به state کامپوننت اضافه شدهاست. سپس برای مثال میتوان این خاصیت را در رویداد componentDidMount از سرور و یا محل ذخیره سازی دیگری دریافت و آنگاه state را بر این اساس به روز رسانی کرد.
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننتهایی که در درخت رندر کامپوننت جاری قرار میگیرند و با کامپوننت Main شروع میشوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده میکنید، انجام میشود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام میدهد. سپس این Provider میتواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق میشوند نیز در دسترس خواهد بود.
یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده میشود؛ اگر Provider متناظر با آن، در درخت کامپوننتهای برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آنها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
خواندن شیء Context در کامپوننتی دیگر
اکنون که یک تامین کنندهی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننتهای محصور شدهی توسط UserProvider، میتوان به صورت زیر عمل کرد:
ابتدا UserConsumer را از ماژول contexts/userContext دریافت میکنیم. سپس برای دسترسی به خاصیت name شیء ارائه شدهی توسط UserProvider، باید قسمتی از متد رندر کامپوننت را توسط شیء UserConsumer، محصور کرد و سپس value آنرا به نحوی که مشاهده میکنید، خواند. Consumer، یک تابع را به عنوان فرزند دریافت میکند. این تابع مقدار شیء تامین شدهی توسط Context را دریافت کرده (همان value={this.state.user} نزدیکترین کامپوننتی که به همراه یک Provider است) و سپس یک المان React را بازگشت میدهد که در این محل رندر خواهد شد.
خروجی برنامه پس از این تغییرات به صورت زیر است:
ساده سازی دسترسی به UserConsumer توسط useContext Hook
نحوهی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارثبری کنند، در بین کامپوننتهای کلاسی و تابعی، یکی است. اما در کامپوننتهای تابعی حداقل میتوان نحوهی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
متد useContext ابتدا شیء UserContext مهیا شدهی توسط ماژول contexts/userContext را دریافت میکند. سپس خروجی آن، همان value تنظیم شدهی توسط نزدیکترین Provider آن در component tree است. این روش، بار ذهنی کمتری را نسبت به حالت قبلی استفادهی از UserConsumer و کار با یک تابع درون آنرا به همراه دارد؛ سادهتر خوانده میشود، سادهتر استفاده میشود. فقط باید دقت داشت که این متد، کل شیء Context را دریافت میکند و نه فقط شیء UserConsumer آنرا.
مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آنها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا میکرد:
اما اکنون با استفاده از useContext، نوشتن و خواندن آن به سادگی چند سطر زیر است که بسیار منطقیتر و عادیتر به نظر میرسد:
ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننتهای فرزند
تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننتهای دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخدادها عمل میکنند، میتوان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنندهی از Context ارسال کرد:
در اینجا ابتدا به خاصیت logout، متدی را نسبت دادهایم که با فراخوانی آن، اطلاعات شیء user موجود در state کامپوننت جاری را پاک میکند. سپس این خاصیت را به صورت یک خاصیت جدید، به شیءای که به ویژگی value شیء UserProvider انتساب داده شده، اضافه میکنیم.
اکنون تمام استفاده کنندههای از این UserProvider میتوانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجهی آن، رندر مجدد کامپوننت و ارائهی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
در این کامپوننت مصرف کنندهی Context، اینبار، مقدار دریافتی، یک شیء با چندین خاصیت است. بنابراین میتوان با استفاده از Object Destructuring، خواص آنرا استخراج و استفاده کرد. برای مثال با انتساب onClick={logoutUser} به دکمهی خروج، این کامپوننت میتواند اطلاعات state و سپس Context ارائه شدهی در کامپوننت App را تغییر دهد.
روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده میکنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت میکند. سپس باید بر اساس آنها، المانهای مدنظر نمایش نام کاربر و دکمهی خروج او را بازگشت داد:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-04.zip
ابتدا شیء user، در بالاترین سطح، دریافت شده و به صفحهای خاص از طریق ویژگیهای props ارسال میشود:
<Page user={user} />
<PageLayout user={user} />
<NavigationBar user={user} />
ایجاد شیء Context در برنامههای React
React Context، راه حلی است جهت به اشتراک گذاری دادهها، در بین انواع و اقسام کامپوننتهای یک برنامه، بدون اینکه نیازی باشد این اطلاعات را توسط props، از یک سطح، به سطحی دیگر، به صورت دستی انتقال داد. برای ایجاد یک نمونهی از آن، ابتدا پوشهی جدید src\contexts را افزوده و سپس فایل src\contexts\userContext.js را درون آن، با محتوای زیر ایجاد میکنیم:
import React from "react"; export const UserContext = React.createContext({ user: {} }); export const UserProvider = UserContext.Provider; export const UserConsumer = UserContext.Consumer;
تامین یک شیء Context در برنامه، در یک کامپوننت کلاسی و یا تابعی
تا اینجا یک شیء Context را به همراه اجزای export شدهی Provider و Consumer آن ایجاد کردیم. اکنون نوبت به پیاده سازی قسمت Provider آن است:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } render() { return ( <> <h1>App Class</h1> <UserProvider value={this.state.user}> <Main /> </UserProvider> </> ); } } export default App;
در ادامه قصد داریم اطلاعات این شیء user موجود در state را با تمام کامپوننتهایی که در درخت رندر کامپوننت جاری قرار میگیرند و با کامپوننت Main شروع میشوند، به اشتراک بگذاریم. این به اشتراک گذاری با import شیء UserProvider از ماژول contexts/userContext به نحوی که مشاهده میکنید، انجام میشود. شیء UserProvider، کار محصور سازی کامپوننت Main را انجام میدهد. سپس این Provider میتواند مقداری را توسط ویژگی value خود دریافت کند که برای مثال در اینجا شیء user است. اکنون این value تا n سطح بعدی که از کامپوننت Main مشتق میشوند نیز در دسترس خواهد بود.
یک نکته: متد React.createContext به همراه یک آرگومان defaultValue اختیاری است که در اختیار Consumerهای آن قرار داده میشود؛ اگر Provider متناظر با آن، در درخت کامپوننتهای برنامه، یافت نشود. یعنی تعریف Provider الزامی نیست. اگر نیاز است مقدار ثابتی را بین چندین کامپوننت به اشتراک بگذارید، فقط کافی است آنها را توسط React.createContext مقدار دهی اولیه کرده و ... استفاده کنید:
export const DefaultRouteContext = React.createContext({ path: '/welcome' });
خواندن شیء Context در کامپوننتی دیگر
اکنون که یک تامین کنندهی Context را ایجاد کردیم، برای خواندن اطلاعات آن در درخت کامپوننتهای محصور شدهی توسط UserProvider، میتوان به صورت زیر عمل کرد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {value => <div>User name: {value.name}.</div>} </UserConsumer> </> ); }
خروجی برنامه پس از این تغییرات به صورت زیر است:
ساده سازی دسترسی به UserConsumer توسط useContext Hook
نحوهی تعریف یک Provider و محصور سازی فرزندانی که باید از آن ارثبری کنند، در بین کامپوننتهای کلاسی و تابعی، یکی است. اما در کامپوننتهای تابعی حداقل میتوان نحوهی دسترسی به UserConsumer را به نحو زیر توسط useContext Hook ساده کرد:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const value = useContext(UserContext); return ( <> <div>User name: {value.name}.</div> </> ); }
مزیت دیگر این روش، ساده سازی کار با چندین شیء Context است. برای مثال اگر دو شیء Context را تعریف کرده باشید، خواندن دو مقدار از آنها، پیشتر چنین شکل تو در تویی را توسط دو Consumer پیدا میکرد:
function HeaderBar() { return ( <CurrentUser.Consumer> {user => <Notifications.Consumer> {notifications => <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> } } </CurrentUser.Consumer> ); }
function HeaderBar() { const user = useContext(CurrentUser); const notifications = useContext(Notifications); return ( <header> Welcome back, {user.name}! You have {notifications.length} notifications. </header> ); }
ارسال اطلاعات به کامپوننت Context Provider، از طریق کامپوننتهای فرزند
تا اینجا با استفاده از React Context، اطلاعات یک Provider را با فرزندان آن به اشتراک گذاشتیم؛ عکس این عمل نیز میسر است. برای اینکار، همانند تمام کامپوننتهای دیگری که برای ارسال اطلاعات به فراخوان خود از طریق رخدادها عمل میکنند، میتوان یک متد رویدادگردان را در کامپوننت والد، به استفاده کنندهی از Context ارسال کرد:
import "../../App.css"; import React, { Component } from "react"; import { UserProvider } from "../../contexts/userContext"; import Main from "./Main2"; class App extends Component { state = { user: { name: "User 1" } }; componentDidMount() { // get user from the server or local storage and then set the currently logged in user to the this.state } logout = () => { console.log("logout"); this.setState({ user: {} }); }; render() { const contextValue = { user: this.state.user, logoutUser: this.logout }; return ( <> <h1>App Class</h1> <UserProvider value={contextValue}> <Main /> </UserProvider> </> ); } } export default App;
اکنون تمام استفاده کنندههای از این UserProvider میتوانند با فراخوانی متد منتسب به logout، سبب پاک شدن اطلاعات کاربر موجود در state کامپوننت App، به روز رسانی state و در نتیجهی آن، رندر مجدد کامپوننت و ارائهی یک UserProvider جدید، با اطلاعاتی جدید به فرزندان آن شوند:
import React, { useContext } from "react"; import { UserContext } from "../../contexts/userContext"; export default function Main() { const { user, logoutUser } = useContext(UserContext); return ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser}> Logout user </button> </> ); }
روش انجام اینکار بدون استفاده از useContext را نیز در ادامه مشاهده میکنید که در ابتدا نیاز به تعریف تابعی را دارد که همان خواص استخراجی را دریافت میکند. سپس باید بر اساس آنها، المانهای مدنظر نمایش نام کاربر و دکمهی خروج او را بازگشت داد:
import React from "react"; import { UserConsumer } from "../../contexts/userContext"; export default function Main(props) { return ( <> <UserConsumer> {({ user, logoutUser }) => ( <> <div>User name: {user.name}.</div> <button type="button" className="btn btn-primary" onClick={logoutUser} > Logout user </button> </> )} </UserConsumer> </> ); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-30-part-04.zip