//define app.service('objUser', function ($http) { this.user = [{ id: null, firstName: null, lastName: null, email: null }]; this.userList = function () { var promise = $http.get('api/user') .success(function (res) { return res; }); return promise; }; }); //call app.controller('UserListCtrl', function ($scope, objUser) { $scope.user = objUser.user; objUser.userList().then(function (promise) { $scope.user = promise.data; }); });
در این پست و با استفاده از سری پستهای آقای مهندس یوسف نژاد در این زمینه، یک Attribute جهت هماهنگ سازی با سیستم اعتبار سنجی ASP.NET MVC در سمت سرور و سمت کلاینت (با استفاده از jQuery Validation) بررسی خواهد شد.
قبل از شروع مطالعه سری پستهای MVC و Entity Framework الزامی است.
برای انجام این کار ابتدا مدل زیر را در برنامه خود ایجاد میکنیم.
using System;
public class SampleModel
{
public DateTime StartDate { get; set; }
public string Data { get; set; }
public int Id { get; set; }
}
using System; using System.ComponentModel.DataAnnotations; public class SampleModel { [Required(ErrorMessage = "Start date is required")] public DateTime StartDate { get; set; } [Required(ErrorMessage = "Data is required")] public string Data { get; set; } public int Id { get; set; } }
@model SampleModel @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } <section> <header> <h3>SampleModel</h3> </header> @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" }) @using (Html.BeginForm("SaveData", "Sample", FormMethod.Post)) { <p> @Html.LabelFor(x => x.StartDate) @Html.TextBoxFor(x => x.StartDate) @Html.ValidationMessageFor(x => x.StartDate) </p> <p> @Html.LabelFor(x => x.Data) @Html.TextBoxFor(x => x.Data) @Html.ValidationMessageFor(x => x.Data) </p> <input type="submit" value="Save"/> } </section>
public class SampleController : Controller { // // GET: /Sample/ public ActionResult Index() { return View(); } public ActionResult SaveData(SampleModel item) { if (ModelState.IsValid) { //save data } else { ModelState.AddModelError("","لطفا خطاهای زیر را برطرف نمایید"); RedirectToAction("Index", item); } return View("Index"); } }
تا اینجای کار روال عادی همیشگی است. اما برای طراحی Attribute دلخواه جهت اعتبار سنجی (مثلا برای مجبور کردن کاربر به وارد کردن یک فیلد با پیام دلخواه و زبان دلخواه) باید یک کلاس جدید تعریف کرده و از کلاس RequiredAttribute ارث ببرد. در پارامتر ورودی این کلاس جهت کار با Resourceهای ثابت در نظر گرفته شده است اما برای اینکه فیلد دلخواه را از دیتابیس بخواند این روش جوابگو نیست. برای انجام آن باید کلاس RequiredAttribute بازنویسی شود.
کلاس طراحی شده باید به صورت زیر باشد:
public class VegaRequiredAttribute : RequiredAttribute, IClientValidatable { #region Fields (2) private readonly string _resourceId; private String _resourceString = String.Empty; #endregion Fields #region Constructors (1) public VegaRequiredAttribute(string resourceId) { _resourceId = resourceId; ErrorMessage = _resourceId; AllowEmptyStrings = true; } #endregion Constructors #region Properties (1) public new String ErrorMessage { get { return _resourceString; } set { _resourceString = GetMessageFromResource(value); } } #endregion Properties #region Methods (2) // Public Methods (1) public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { yield return new ModelClientValidationRule { ErrorMessage = GetMessageFromResource(_resourceId), ValidationType = "required" }; } // Private Methods (1) private string GetMessageFromResource(string resourceId) { var errorMessage = HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string; return errorMessage ?? ErrorMessage; } #endregion Methods }
1- ابتدا دستور
HttpContext.GetGlobalResourceObject(_resourceId, "Yes") as string;
که عنوان کلید Resource را از سازنده کلاس گرفته (کد اقای یوسف نژاد) رشته معادل آن را از دیتابیس بازیابی میکند.
2- ارث بری از اینترفیس IClientValidatable، در صورتی که از این اینترفیس ارث بری نداشته باشیم. Validator طراحی شده در طرف کلاینت کار نمیکند. بلکه کاربر با کلیک بروی دکمه مورد نظر دادهها را به سمت سرور ارسال میکند. در صورت وجود خطا در پست بک خطا نمایش داده خواهد شد. اما با ارث بری از این اینترفیس و پیاده سازی متد GetClientValidationRules میتوان تعریف کرد که در طرف کلاینت با استفاده از Unobtrusive jQuery پیام خطای مورد نظر به کنترل ورودی مورد نظر (مانند تکست باکس) اعمال میشود. مثلا در این مثال خصوصیت data-val-required به input هایی که قبلا در مدل ما Reqired تعریف شده اند اعمال میشود.
حال در مدل تعریف شده میتوان به جای Required میتوان از VegaRequiredAttribute مانند زیر استفاده کرد. (همراه با نام کلید مورد نظر در دیتابیس)
public class SampleModel { [VegaRequired("RequiredMessage")] public DateTime StartDate { get; set; } [VegaRequired("RequiredMessage")] public string Data { get; set; } public int Id { get; set; } }
با اجرای برنامه اینبار خروجی به شکل زیر خواهد بود.
جهت فعال ساری اعتبار سنجی سمت کلاینت ابتدا باید اسکریپتهای زیر به صفحه اضافه شود.
<script src="@Url.Content("~/Scripts/jquery-1.9.1.min.js")" type="text/javascript"></script> <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>
<appSettings> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> </appSettings>
<script type="text/javascript"> jQuery.validator.addMethod('required', function (value, element, params) { if (value == null | value == "") { return false; } else { return true; } }, ''); jQuery.validator.unobtrusive.adapters.add('required', {}, function (options) { options.rules['required'] = true; options.messages['required'] = options.message; }); </script>
موفق و موید باشید.
منابع ^ و ^ و ^ و ^
وبلاگها ، سایتها و مقالات ایرانی (داخل و خارج از ایران)
- تنظیم Http Proxy در CVS
- URL Rewrite Module for IIS 7.0
- تریگرها در SQL Server
- گستره سیاست ها در Group Policy
- لینوس توروالدز به سوی میزکار گنوم باز می گردد
- ابر برچسبها و مقایسهی سخنرانیهای بوش و اوباما
- اندیسهای فیلتر شده
ASP. Net
- ASP.NET MVC 1.0 Release Candidate
- ویدیوی معرفی ASP.NET MVC و ویدیویی دیگر
- GridView and CommandArguments
طراحی و توسعه وب
PHP
- PDT 2.0 : ابزارهای جدیدی برای برنامه نویسهای PHP
- NetBeans ، PHP و کار با دیتابیس
- بررسی سرعت و میزان مصرف حافظهی متدهای مختلف در PHP
سی شارپ
عمومی دات نت
ویندوز
(ایکاش بجای تمام اینکارها یک سیستم سادهتر توسعهی پلاگین برای آن طراحی میکردند ... یا به عبارتی یکی از مهمترین دلیلهای اقبال مردم به فایرفاکس را به صورت بسیار کم رنگی دارد)
مسایل اجتماعی و انسانی برنامه نویسی
- داستان استفاده از برنامههای خود پیش از استفاده دیگران
- راههایی برای افزایش بهره وری
- راههایی برای کاهش بهره وری!
متفرقه
String.format = function () { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { s = s.replace("{" + i + "}", arguments[i + 1]); } return s; };
String.format = function () { var s = arguments[0]; for (var arg in arguments) { var i = parseInt(arg); s = s.replace("{" + i + "}", arguments[i + 1]); } return s; };
console.log(String.format("{0} is nice!", "donettips.info"));
donettips.info is nice!
console.log(String.format("{0} is {1} nice! {0} is {1} nice!", "donettips.info", "very"));
donettips.info is very nice! {0} is {1} nice!
String.format = function () { var original = arguments[0], replaced; for (var i = 0; i < arguments.length - 1; i++) { replaced = ''; while (replaced != original) { original = replaced || original; replaced = original.replace("{" + i + "}", arguments[i + 1]); } } return replaced; };
donettips.info is very nice! donettips.info is very nice!
String.format = function () { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { s = s.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i + 1]); } return s; };
String.format = function () { var s = arguments[0], i = arguments.length - 1; while (i--) { s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i + 1]); } return s; };
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "{2}", "two"));
zero:0 {2}:1 two:2
zero:0 two:1 two:2
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "one", "{1}"));
zero:0 one:1 one:2
zero:0 one:1 {1}:2
String.format = function () { var args = arguments; return args[0].replace(/{(\d+)}/g, function (match, number) { return args[parseInt(number) + 1]; }); };
console.log(String.format("{0} is {1} nice!", "donettips.info"));
donettips.info is undefined nice!
String.format = function () { var s = arguments[0], args = arguments; return s.replace(/{(\d+)}/g, function (match, number) { var i = parseInt(number); return typeof args[i + 1] != 'undefined' ? args[i + 1] : match; }); };
console.log(String.format("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}} {{{{2}}}} {2}", "zero", "{2}", "two"));
zero:0 {2}:1 two:2, {zero} {{{2}}} {{{two}}} two
String.format = function () { var s = arguments[0], args = arguments; return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) { if (match == "{{") { return "{"; } if (match == "}}") { return "}"; } var i = parseInt(number); return typeof args[i + 1] != 'undefined' ? args[i + 1] : match; }); };
zero:0 {2}:1 two:2, {0} {{2}} {{2}} two
String.prototype.format = function () { ... }
String.prototype.format = function () { var s = this.toString(), args = arguments; return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) { if (match == "{{") { return "{"; } if (match == "}}") { return "}"; } return typeof args[number] != 'undefined' ? args[number] : match; }); };
console.log("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}} {{{{2}}}} {2}".format("zero", "{2}", "two"));
String.format = function () { var s = arguments[0], args = arguments[1]; for (var arg in args) { s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]); } return s; };
String.prototype.format = function () { var s = this.toString(), args = arguments[0]; for (var arg in args) { s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]); } return s; };
console.log(String.format("{site} is {adj}! {site} is {adj}!", { site: "donettips.info", adj: "nice" })); console.log("{site} is {adj}! {site} is {adj}!".format({ site: "donettips.info", adj: "nice" }));
String.format = function String$format(format, args) { /// <summary locid="M:J#String.format" /> /// <param name="format" type="String"></param> /// <param name="args" parameterArray="true" mayBeNull="true"></param> /// <returns type="String"></returns> // var e = Function._validateParams(arguments, [ // { name: "format", type: String }, // { name: "args", mayBeNull: true, parameterArray: true } // ]); // if (e) throw e; return String._toFormattedString(false, arguments); }; String._toFormattedString = function String$_toFormattedString(useLocale, args) { var result = ''; var format = args[0]; for (var i = 0; ; ) { var open = format.indexOf('{', i); var close = format.indexOf('}', i); if ((open < 0) && (close < 0)) { result += format.slice(i); break; } if ((close > 0) && ((close < open) || (open < 0))) { if (format.charAt(close + 1) !== '}') { throw Error.argument('format', Sys.Res.stringFormatBraceMismatch); } result += format.slice(i, close + 1); i = close + 2; continue; } result += format.slice(i, open); i = open + 1; if (format.charAt(i) === '{') { result += '{'; i++; continue; } if (close < 0) throw Error.argument('format', Sys.Res.stringFormatBraceMismatch); var brace = format.substring(i, close); var colonIndex = brace.indexOf(':'); var argNumber = parseInt((colonIndex < 0) ? brace : brace.substring(0, colonIndex), 10) + 1; if (isNaN(argNumber)) throw Error.argument('format', Sys.Res.stringFormatInvalid); var argFormat = (colonIndex < 0) ? '' : brace.substring(colonIndex + 1); var arg = args[argNumber]; if (typeof (arg) === "undefined" || arg === null) { arg = ''; } if (arg.toFormattedString) { result += arg.toFormattedString(argFormat); } else if (useLocale && arg.localeFormat) { result += arg.localeFormat(argFormat); } else if (arg.format) { result += arg.format(argFormat); } else result += arg.toString(); i = close + 1; } return result; }
console.log(String.format("{0:n}, {0:c}, {0:p}, {0:d}", 100.0001)); // result: 100.00, ¤100.00, 10,000.01 %, 100.0001 console.log(String.format("{0:d}, {0:t}", new Date(2015, 1, 1, 10, 45))); // result: 02/01/2015, 10:45
var template = jQuery.validator.format("{0} is not a valid value"); console.log(template("abc")); // result: 'abc is not a valid value'
String.format([full format string], [arguments...]); // or: [date|number].format([partial format string]);
// Object path String.format("Welcome back, {username}!", { id: 3, username: "JohnDoe" }); // Result: "Welcome back, JohnDoe!" // Date/time formatting String.format("The time is now {0:t}.", new Date(2009, 5, 1, 13, 22)); // Result: "The time is now 01:22 PM." // Date/time formatting (without using a full format string) var d = new Date(); d.format("hh:mm:ss tt"); // Result: "02:28:06 PM" // Custom number format string String.format("Please call me at {0:+##0 (0) 000-00 00}.", 4601111111); // Result: "Please call me at +46 (0) 111-11 11." // Another custom number format string String.format("The last year result was {0:+$#,0.00;-$#,0.00;0}.", -5543.346); // Result: "The last year result was -$5,543.35." // Alignment String.format("|{0,10:PI=0.00}|", Math.PI); // Result: "| PI=3.14|" // Rounding String.format("1/3 ~ {0:0.00}", 1/3); // Result: "1/3 ~ 0.33" // Boolean values String.format("{0:true;;false}", 0); // Result: "false" // Explicitly specified localization // (note that you have to include the .js file for used cultures) msf.setCulture("en-US"); String.format("{0:#,0.0}", 3641.667); // Result: "3,641.7" msf.setCulture("sv-SE"); String.format("{0:#,0.0}", 3641.667); // Result: "3 641,7"
//inline arguments String.format("some string with {0} and {1} injected using argument {{number}}", 'first value', 'second value'); //returns: 'some string with first value and second value injected argument {number}' //single array String.format("some string with {0} and {1} injected using array {{number}}", [ 'first value', 'second value' ]); //returns: 'some string with first value and second value injected using array {number}' //single object String.format("some string with {first} and {second} value injected using {{propertyName}}",{first:'first value',second:'second value'}); //returns: 'some string with first value and second value injected using {propertyName}'
هدف بررسی کامل مباحث Streaming در دات نت فریمورک میباشد.
Stream چیست؟
دنبالهای از بایتها که میتوان آنها را از یک backing store (انبار پشتیبان) خواند یا در آن نوشت.
Backing Store
یک رسانه ذخیره سازی از جمله Disk-Drive، Memory و Network Location میباشد که به عنوان منبع یا مقصدی برای خواندن و نوشتن بایتها به صورت دنبالهای، میتوان از آن استفاده کرد.
زمانی که قرار است داده ذخیره شده به صورت Stream مصرف شود، مزیت مقیاس پذیری را نیز خواهید داشت. لذا لازم نیست با مشکل محدودیت حافظه نیز درگیر شوید.
آشنایی با معماری Streaming در دات نت
Streaming در دات نت، توسط سه مفهوم: backing store، decorators و adapters در برگرفته شده است.
کلاسی به نام Stream در دات نت، برای ارائه یکسری متد مشترک برای Reading، Writing و Positioning در نظر گرفته شده است که همچنین کلاس پایه Backing Store Streams و Decorator Streams نیز میباشد.
اعضای کلاس Stream را میتوان به شکل زیر گروه بندی کرد:
در نظر داشته باشید که Stream ها، دارای اشاره گری به مکان جاری تحت عنوان Pointer نیز میباشند. مقدار پیش فرض آن «صفر» میباشد و زمانی که شروع به خواندن از Stream کنید، این خواندن از مکانی شروع میشود که Pointer به آنجا اشاره میکند. به شکل زیر توجه کنید:
اگر قرار باشد 3 بایت اول خوانده شود، لذا حالت زیر را خواهیم داشت:
همانطور که مشخص است، Pointer مربوط به Stream به اولین خانهای اشاره میکند که در Readهای بعدی قرار است خوانده شود. در نهایت با خواندن دو بایت دیگر، حالت زیر را خواهیم داشت:
برای Reading و Writing متدهای زیر در کلاس System.IO.Stream در نظر گرفته شدهاند:
(Read(byte[] buffer,int offset,int count
buffer: آرایهای از بایتها برای نگهداری دادهی خوانده شده از Stream
offset: برخلاف تصور، اندیسی است که مکان شروع ذخیره سازی در buffer را مشخص میکند و نه مکان شروع خواندن از Stream
count: بیشترین تعداد بایت برای خواندن از Stream میباشد. با توجه به اینکه ممکن است به انتهای Stream رسیده باشیم یا اینکه در شرایطی مثلا در Network Streamها چه بسا خود Stream تصمیم بگیرد تعداد بایت کمتری از این مقدار Count را برای ما ارائه دهد. از این رو همیشه مقداری که برای Count مشخص میکنید همان مقداری نیست که متد Read برای شما برگشت خواهد داد.
return: تعداد بایتهایی که خوانده شده است یا اگر به انتهای Stream رسیده باشیم «0» برگشت خواهد داد. از این رو تکه کد زیر برای خواندن کل داده به یکباره، قابل اطمینان نخواهد بود.
byte[] dataToRead=new byte[stream.Length]; int bytesRead=stream.Read(dataToRead,0,dataToRead.Length);
راه حل جایگزین میتواند به شکل زیر باشد:
static byte[] ReadBytes(Stream stream) { // dataToRead will hold the data read from the stream byte[] dataToRead = new byte[stream.Length]; //this is the total number of bytes read. this will be incremented //and eventually will equal the bytes size held by the stream int totalBytesRead = 0; //this is the number of bytes read in each iteration (i.e. chunk size) int chunkBytesRead = 1; while (totalBytesRead < dataToRead.Length && chunkBytesRead > 0) { chunkBytesRead = stream.Read(dataToRead, totalBytesRead, dataToRead.Length - totalBytesRead); totalBytesRead = totalBytesRead + chunkBytesRead; } return dataToRead; }
byte[] data = new BinaryReader (s).ReadBytes (1000);
return: یک بایت را از مکان فعلی که Pointer به آن اشاره میکند، میخواند. اگر خروجی «-1» باشد، به انتهای Stream رسیده اید.
برخلاف انتظار، خروجی این متد از نوع int میباشد؛ چرا که لازم است «-1» را نیز در برگیرد.
با توجه به حالت FileStream که فقط برای Append کردن وهله سازی شده است، امکان خواندن را نخواهید داشت. بنابراین زمانیکه از کلاس شخص ثالثی برای خواندن از Stream استفاده میکنید، بهصلاح است (به منظور Defensive Programming) که از متد CanRead قبل خواندن بهره ببرید.
(Write(byte[] array,int offset,int count
array: آرایه ای از بایتها که قرار است در Stream درج شوند.
offset: اندیس شروع array برای درج کردن در Stream را مشخص میکند.
count: بیشترین تعداد بایتی که از array در Stream درج خواهد شد.
برای درج یک بایت در Stream استفاده میشود.
برای تشخص پشتیبانی کردن Stream از عملیات درج کردن مورد استفاده قرار خواهد گرفت.
با انجام هر یک از عملیات Read و Write برروی Stream، باعث تغییر مکان Pointer مربوط به آن خواهید شد. در صورتیکه نیاز است به صورت انتخابی مکان خاصی از Stream را برای شروع درج کردن یا خواندن انتخاب کنید، Seeking کمک کننده خواهد بود.
باید توجه داشت که پشتیبانی از این عملیات به backing store مورد استفاده وابسته میباشد. از این رو باید دانست که MemoryStream و FileStream از Seeking پشتیبانی کرده ولی در مقابل NetworkStream، PipeStream و همچنین Decorator Streams به غیر از BufferedStream قابلیت Seeking را ندارند. BufferedStream با ایجاد پوششی برروی یک Stream به اصطلاح non-seekable، امکان Seeking درون Buffer داخلی خود را مهیا خواهد کرد.
برای عملیات Seeking نیز اعضایی در کلاس پایه System.IO.Stream در نظر گرفته شده است:
برای تنظیم مکان Pointer در Stream استفاده خواهد شد.
متدی برای تنظیم طول Stream، که اگر value ارسال شده کوچکتر از طول فعلی Stream باشد، آن را کوتاه کرده و در غیر این صورت، Stream موردنظر گسترش خواهد یافت. برای استفاده از این متد، Stream مورد نظر باید قابلیت Writing و Seeking را داشته باشد.
پراپرتی فقط خواندنی که طول Stream را مشخص میکند. در صورتیکه Stream مورد نظر Seekable باشد، میتوان از این پراپرتی بهر برد؛ این بدین معنی است که اگر با یک Stream از نوع non-seekable کار میکنید، در صورت استفاده از این خصوصیت، تمام بایتهای Stream خوانده شده و بعد از قرار گرفتن در یک buffer (به عنوان مثال در memory)، محاسبه خواهد شد.
Position
پراپرتی برای خواندن یا تنظیم مکان فعلی Pointer مربوط به Stream، میباشد. برای استفاده از آن لازم است Stream مورد استفاده Seekable باشد.
مشخص میکند که Stream مورد استفاده Seekable می باشد یا خیر.
به طور خلاصه با استفاده از متد Seek انعطاف پذیری بالایی خواهید داشت. با مقدار دهی پراپرتی Position، این مقدار همیشه نسبت به ابتدای Stream در نظر گرفته خواهد شد (شکل زیر)؛ این در حالی است که با استفاده از متد Seek میتوان مشخص کرد که مقدار Offset تنظیم شده نسبت به ابتدا، مکان جاری و یا انتهای Stream میباشد.
مثال:
using (FileStream fs = File.Create(@"C:\files\testfile3.txt")) { // position is 0 long pos = fs.Position; // sets the position to 1 fs.Position = 1; byte[] arrbytes = { 100, 101 }; //writes the content of arrbytes into current position - which is 1 fs.Write(arrbytes, 0, arrbytes.Length); //position is now 3 as its advanced by write pos = fs.Position; fs.Position = 0; byte[] readdata1 = ReadBytes(fs); }
Closing and Flushing
کلاس پایه System.IO.Stream اینترفیس IDisposable را پیاده سازی کرده است؛ لذا بهتر است برای آزاد سازی منابع از جمله: file handle در FileStream یا socket handle در NetworkStream، بعد از استفاده، متد Dispose آنها را فراخوانی کنید یا با وهله سازی آنها در بدنه using، این فراخوانی به صورت ضمنی انجام شود.
نکته: باید توجه کنید که با Close (معادل Dispose) شدن decorator streamها ، backing store stream داخلی آنها نیز Close خواهد شد.
با توجه به اینکه I/O عملیات پرهزینهای میباشد، برخی از انواع Streamها به منظور بهبود کارآیی از یک مکانیزم بافر داخلی استفاده میکنند. به این شکل که عملیات Write، داده را به جای آنکه درون backing store ذخیره سازی کند، درون این بافر ذخیره سازی خواهد کرد. زمانیکه این بافر پر شود یا به صورت صریح متدهای Flush یا Close فراخوانی شده باشند، داده موجود در بافر درون backing store ذخیره خواهد شد. در نتیجه عملیات Read هم میتواند به بخشی از داده اصلی که هم اکنون درون بافر میباشد، دسترسی سریعتری داشته باشد. به عنوان مثال FileStream از این مکانیزم داخلی برخوردار است. سایز پیش فرض این بافر 4KB (قابل تنظیم است) میباشد. برای سایر مواردی که این امکان برایشان وجود ندارد، میتوان از BufferedStream برای Decorate کردن Stream مورد نظر خود استفاده کرد.
نکته: به صورت پیش فرض، Streamها thread-safe نیستند و امکان خواندن و نوشتن همزمان توسط چند thread برروی یک stream مشترک را نخواهید داشت. برای حل این موضوع، متد استاتیکی در کلاس Stream تحت عنوان Synchronized در نظر گرفته شده است که یک thread-safe wrapper را به برروی stream ورودی در نظر گرفته و آن را به عنوان خروجی برگشت خواهد داد.
[HostProtection(SecurityAction.LinkDemand, Synchronization = true)] public static Stream Synchronized(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); if (stream is Stream.SyncStream) return stream; return (Stream) new Stream.SyncStream(stream); }
تهیه فیلتر سفارشی SiteAuthorize
برای بررسی اعمال Ajaxایی، نیاز است فیلتر پیش فرض Authorize سفارشی شود:
using System; using System.Net; using System.Web.Mvc; namespace MvcApplication28.Helpers { [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public sealed class SiteAuthorizeAttribute : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAuthenticated) { throw new UnauthorizedAccessException(); //to avoid multiple redirects } else { handleAjaxRequest(filterContext); base.HandleUnauthorizedRequest(filterContext); } } private static void handleAjaxRequest(AuthorizationContext filterContext) { var ctx = filterContext.HttpContext; if (!ctx.Request.IsAjaxRequest()) return; ctx.Response.StatusCode = (int)HttpStatusCode.Forbidden; ctx.Response.End(); } } }
using System.Web.Mvc; using MvcApplication28.Helpers; namespace MvcApplication28.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } [SiteAuthorize] [HttpPost] public ActionResult SaveData(string data) { if(string.IsNullOrWhiteSpace(data)) return Content("NOk!"); return Content("Ok!"); } } }
@{ ViewBag.Title = "Index"; var postUrl = this.Url.Action(actionName: "SaveData", controllerName: "Home"); } <h2> Index</h2> @using (Html.BeginForm(actionName: "SaveData", controllerName: "Home", method: FormMethod.Post, htmlAttributes: new { id = "form1" })) { @Html.TextBox(name: "data") <br /> <span id="btnSave">Save Data</span> } @section Scripts { <script type="text/javascript"> $(document).ready(function () { $("#btnSave").click(function (event) { $.ajax({ type: "POST", url: "@postUrl", data: $("#form1").serialize(), // controller is returning a simple text, not json complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = "/login"; } } }); }); }); </script> }
نکته تکمیلی:
در متد handleAjaxRequest، میتوان یک JavaScriptResult را نیز بازگشت داد تا همان کدهای مرتبط با window.location را به صورت خودکار به صفحه تزریق کند:
filterContext.Result = new JavaScriptResult { Script="window.location = '" + redirectToUrl + "'"};
کلمههای عبور کاربران فعلی سیستم با الگوریتمی متفاوت از الگوریتم مورد استفاده Identity هش شدهاند. برای اینکه کاربرانی که قبلا ثبت نام کرده بودند بتوانند با کلمههای عبور خود وارد سایت شوند، باید الگوریتم هش کردن Identity را با الگوریتم فعلی مورد استفاده Iris جایگزین کرد.
برای تغییر روش هش کردن کلمات عبور در Identity باید اینترفیس IPasswordHasher را پیاده سازی کنید:
public class IrisPasswordHasher : IPasswordHasher { public string HashPassword(string password) { return Utilities.Security.Encryption.EncryptingPassword(password); } public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { return Utilities.Security.Encryption.VerifyPassword(providedPassword, hashedPassword) ? PasswordVerificationResult.Success : PasswordVerificationResult.Failed; } }
سپس باید وارد کلاس ApplicationUserManager شده و در سازندهی آن اینترفیس IPasswordHasher را به عنوان وابستگی تعریف کنید:
public ApplicationUserManager(IUserStore<ApplicationUser, int> store, IUnitOfWork uow, IIdentity identity, IApplicationRoleManager roleManager, IDataProtectionProvider dataProtectionProvider, IIdentityMessageService smsService, IIdentityMessageService emailService, IPasswordHasher passwordHasher) : base(store) { _store = store; _uow = uow; _identity = identity; _users = _uow.Set<ApplicationUser>(); _roleManager = roleManager; _dataProtectionProvider = dataProtectionProvider; this.SmsService = smsService; this.EmailService = emailService; PasswordHasher = passwordHasher; createApplicationUserManager(); }
برای اینکه کلاس IrisPasswordHasher را به عنوان نمونه درخواستی IPasswordHasher معرفی کنیم، باید در تنظیمات StructureMap کد زیر را نیز اضافه کنید:
x.For<IPasswordHasher>().Use<IrisPasswordHasher>();
پیاده سازی اکشن متد ثبت نام کاربر با استفاده از Identity
در کنترلر UserController، اکشن متد Register را به شکل زیر بازنویسی کنید:
[HttpPost] [ValidateAntiForgeryToken] [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")] public virtual async Task<ActionResult> Register(RegisterModel model) { if (ModelState.IsValid) { var user = new ApplicationUser { CreatedDate = DateAndTime.GetDateTime(), Email = model.Email, IP = Request.ServerVariables["REMOTE_ADDR"], IsBaned = false, UserName = model.UserName, UserMetaData = new UserMetaData(), LastLoginDate = DateAndTime.GetDateTime() }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { var addToRoleResult = await _userManager.AddToRoleAsync(user.Id, "user"); if (addToRoleResult.Succeeded) { var code = await _userManager.GenerateEmailConfirmationTokenAsync(user.Id); var callbackUrl = Url.Action("ConfirmEmail", "User", new { userId = user.Id, code }, protocol: Request.Url.Scheme); _emailService.SendAccountConfirmationEmail(user.Email, callbackUrl); return Json(new { result = "success" }); } addErrors(addToRoleResult); } addErrors(result); } return PartialView(MVC.User.Views._Register, model); }
برای این کار متد زیر را به کلاس EmailService اضافه کنید:
public SendingMailResult SendAccountConfirmationEmail(string email, string link) { var model = new ConfirmEmailModel() { ActivationLink = link }; var htmlText = _viewConvertor.RenderRazorViewToString(MVC.EmailTemplates.Views._ConfirmEmail, model); var result = Send(new MailDocument { Body = htmlText, Subject = "تایید حساب کاربری", ToEmail = email }); return result; }
@model Iris.Model.EmailModel.ConfirmEmailModel <div style="direction: rtl; -ms-word-wrap: break-word; word-wrap: break-word;"> <p>با سلام</p> <p>برای فعال سازی حساب کاربری خود لطفا بر روی لینک زیر کلیک کنید:</p> <p>@Model.ActivationLink</p> <div style=" color: #808080;"> <p>با تشکر</p> <p>@Model.SiteTitle</p> <p>@Model.SiteDescription</p> <p><span style="direction: ltr !important; unicode-bidi: embed;">@Html.ConvertToPersianDateTime(DateTime.Now, "s,H")</span></p> </div> </div>
اصلاح پیام موفقیت آمیز بودن ثبت نام کاربر جدید
سیستم IRIS از ارسال ایمیل تایید حساب کاربری استفاده نمیکند و به محض اینکه عملیات ثبت نام تکمیل میشد، صفحه رفرش میشود. اما در سیستم Identity یک ایمیل حاوی لینک فعال سازی حساب کاربری به او ارسال میشود.
برای اصلاح پیغام پس از ثبت نام، باید به فایل myscript.js درون پوشهی Scripts مراجعه کرده و رویداد onSuccess شیء RegisterUser را به شکل زیراصلاح کنید:
RegisterUser.Form.onSuccess = function (data) { if (data.result == "success") { var message = '<div id="alert"><button type="button" data-dismiss="alert">×</button>ایمیلی حاوی لینک فعال سازی، به ایمیل شما ارسال شد؛ لطفا به ایمیل خود مراجعه کرده و بر روی لینک فعال سازی کلیک کنید.</div>'; $('#registerResult').html(message); } else { $('#logOnModal').html(data); } };
[AllowAnonymous] public virtual async Task<ActionResult> ConfirmEmail(int? userId, string code) { if (userId == null || code == null) { return View("Error"); } var result = await _userManager.ConfirmEmailAsync(userId.Value, code); return View(result.Succeeded ? "ConfirmEmail" : "Error"); }
@{ ViewBag.Title = "حساب کاربری شما تایید شد"; } <h2>@ViewBag.Title.</h2> <div> <p> با تشکر از شما، حساب کاربری شما تایید شد. </p> <p> @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" }) </p> </div>
اصلاح اکشن متد ورود به سایت
[HttpPost] [ValidateAntiForgeryToken] public async virtual Task<ActionResult> LogOn(LogOnModel model, string returnUrl) { if (!ModelState.IsValid) { if (Request.IsAjaxRequest()) return PartialView(MVC.User.Views._LogOn, model); return View(model); } const string emailRegPattern = @"^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$"; string ip = Request.ServerVariables["REMOTE_ADDR"]; SignInStatus result = SignInStatus.Failure; if (Regex.IsMatch(model.Identity, emailRegPattern)) { var user = await _userManager.FindByEmailAsync(model.Identity); if (user != null) { result = await _signInManager.PasswordSignInAsync (user.UserName, model.Password, model.RememberMe, shouldLockout: true); } } else { result = await _signInManager.PasswordSignInAsync(model.Identity, model.Password, model.RememberMe, shouldLockout: true); } switch (result) { case SignInStatus.Success: if (Request.IsAjaxRequest()) return JavaScript(IsValidReturnUrl(returnUrl) ? string.Format("window.location ='{0}';", returnUrl) : "window.location.reload();"); return redirectToLocal(returnUrl); case SignInStatus.LockedOut: ModelState.AddModelError("", string.Format("حساب شما قفل شد، لطفا بعد از {0} دقیقه دوباره امتحان کنید.", _userManager.DefaultAccountLockoutTimeSpan.Minutes)); break; case SignInStatus.Failure: ModelState.AddModelError("", "نام کاربری یا کلمه عبور اشتباه است."); break; default: ModelState.AddModelError("", "در ورود شما خطایی رخ داده است."); break; } if (Request.IsAjaxRequest()) return PartialView(MVC.User.Views._LogOn, model); return View(model); }
اصلاح اکشن متد خروج کاربر از سایت
[HttpPost] [ValidateAntiForgeryToken] [Authorize] public virtual ActionResult LogOut() { _authenticationManager.SignOut(); if (Request.IsAjaxRequest()) return Json(new { result = "true" }); return RedirectToAction(MVC.User.ActionNames.LogOn, MVC.User.Name); }
پیاده سازی ریست کردن کلمهی عبور با استفاده از ASP.NET Identity
مکانیزم سیستم IRIS برای ریست کردن کلمهی عبور به هنگام فراموشی آن، ساخت GUID و ذخیرهی آن در دیتابیس است. سیستم Identity با استفاده از یک توکن رمز نگاری شده و بدون استفاده از دیتابیس، این کار را انجام میدهد و با استفاده از قابلیتهای تو کار سیستم Identity، تمهیدات امنیتی بهتری را نسبت به سیستم کنونی در نظر گرفته است.
برای این کار کدهای کنترلر ForgottenPasswordController را به شکل زیر ویرایش کنید:
using System.Threading.Tasks; using System.Web.Mvc; using CaptchaMvc.Attributes; using Iris.Model; using Iris.Servicelayer.Interfaces; using Iris.Web.Email; using Microsoft.AspNet.Identity; namespace Iris.Web.Controllers { public partial class ForgottenPasswordController : Controller { private readonly IEmailService _emailService; private readonly IApplicationUserManager _userManager; public ForgottenPasswordController(IEmailService emailService, IApplicationUserManager applicationUserManager) { _emailService = emailService; _userManager = applicationUserManager; } [HttpGet] public virtual ActionResult Index() { return PartialView(MVC.ForgottenPassword.Views._Index); } [HttpPost] [ValidateAntiForgeryToken] [CaptchaVerify("تصویر امنیتی وارد شده معتبر نیست")] public async virtual Task<ActionResult> Index(ForgottenPasswordModel model) { if (!ModelState.IsValid) { return PartialView(MVC.ForgottenPassword.Views._Index, model); } var user = await _userManager.FindByEmailAsync(model.Email); if (user == null || !(await _userManager.IsEmailConfirmedAsync(user.Id))) { // Don't reveal that the user does not exist or is not confirmed return Json(new { result = "false", message = "این ایمیل در سیستم ثبت نشده است" }); } var code = await _userManager.GeneratePasswordResetTokenAsync(user.Id); _emailService.SendResetPasswordConfirmationEmail(user.UserName, user.Email, code); return Json(new { result = "true", message = "ایمیلی برای تایید بازنشانی کلمه عبور برای شما ارسال شد.اعتبارایمیل ارسالی 3 ساعت است." }); } [AllowAnonymous] public virtual ActionResult ResetPassword(string code) { return code == null ? View("Error") : View(); } [AllowAnonymous] public virtual ActionResult ResetPasswordConfirmation() { return View(); } // // POST: /Account/ResetPassword [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> ResetPassword(ResetPasswordViewModel model) { if (!ModelState.IsValid) { return View(model); } var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { // Don't reveal that the user does not exist return RedirectToAction("Error"); } var result = await _userManager.ResetPasswordAsync(user.Id, model.Code, model.Password); if (result.Succeeded) { return RedirectToAction("ResetPasswordConfirmation", "ForgottenPassword"); } addErrors(result); return View(); } private void addErrors(IdentityResult result) { foreach (var error in result.Errors) { ModelState.AddModelError("", error); } } } }
همچنین برای اکشن متدهای اضافه شده، Viewهای زیر را نیز باید اضافه کنید:
- View با نام ResetPasswordConfirmation.cshtml را اضافه کنید.
@{ ViewBag.Title = "کلمه عبور شما تغییر کرد"; } <hgroup> <h1>@ViewBag.Title.</h1> </hgroup> <div> <p> کلمه عبور شما با موفقیت تغییر کرد </p> <p> @Ajax.ActionLink("ورود / ثبت نام", MVC.User.ActionNames.LogOn, MVC.User.Name, new { area = "", returnUrl = Html.ReturnUrl(Context, Url) }, new AjaxOptions { HttpMethod = "GET", InsertionMode = InsertionMode.Replace, UpdateTargetId = "logOnModal", LoadingElementDuration = 300, LoadingElementId = "loadingMessage", OnSuccess = "LogOnForm.onSuccess" }, new { role = "button", data_toggle = "modal", data_i_logon_link = "true", rel = "nofollow" }) </p> </div>
- View با نام ResetPassword.cshtml
@model Iris.Model.ResetPasswordViewModel @{ ViewBag.Title = "ریست کردن کلمه عبور"; } <h2>@ViewBag.Title.</h2> @using (Html.BeginForm("ResetPassword", "ForgottenPassword", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { @Html.AntiForgeryToken() <h4>ریست کردن کلمه عبور</h4> <hr /> @Html.ValidationSummary("", new { @class = "text-danger" }) @Html.HiddenFor(model => model.Code) <div> @Html.LabelFor(m => m.Email, "ایمیل", new { @class = "control-label" }) <div> @Html.TextBoxFor(m => m.Email) </div> </div> <div> @Html.LabelFor(m => m.Password, "کلمه عبور", new { @class = "control-label" }) <div> @Html.PasswordFor(m => m.Password) </div> </div> <div> @Html.LabelFor(m => m.ConfirmPassword, "تکرار کلمه عبور", new { @class = "control-label" }) <div> @Html.PasswordFor(m => m.ConfirmPassword) </div> </div> <div> <div> <input type="submit" value="تغییر کلمه عبور" /> </div> </div> }
همچنین این View و Controller متناظر آن، احتیاج به ViewModel زیر دارند که آن را به پروژهی Iris.Models اضافه کنید.
using System.ComponentModel.DataAnnotations; namespace Iris.Model { public class ResetPasswordViewModel { [Required] [EmailAddress] [Display(Name = "ایمیل")] public string Email { get; set; } [Required] [StringLength(100, ErrorMessage = "کلمه عبور باید حداقل 6 حرف باشد", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "کلمه عبور")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "تکرار کلمه عبور")] [Compare("Password", ErrorMessage = "کلمه عبور و تکرارش یکسان نیستند")] public string ConfirmPassword { get; set; } public string Code { get; set; } } }
حذف سیستم قدیمی احراز هویت
برای حذف کامل سیستم احراز هویت IRIS، وارد فایل Global.asax.cs شده و سپس از متد Application_AuthenticateRequest کدهای زیر را حذف کنید:
var principalService = ObjectFactory.GetInstance<IPrincipalService>(); var formsAuthenticationService = ObjectFactory.GetInstance<IFormsAuthenticationService>(); context.User = principalService.GetCurrent()
فارسی کردن خطاهای ASP.NET Identity
سیستم Identity، پیامهای خطاها را از فایل Resource موجود در هستهی خود، که به طور پیش فرض، زبان آن انگلیسی است، میخواند. برای مثال وقتی ایمیلی تکراری باشد، پیامی به زبان انگلیسی دریافت خواهید کرد و متاسفانه برای تغییر آن، راه سر راست و واضحی وجود ندارد. برای تغییر این پیامها میتوان از سورس باز بودن Identity استفاده کنید و قسمتی را که پیامها را تولید میکند، خودتان با پیامهای فارسی باز نویسی کنید.
راه اول این است که از این پروژه استفاده کرد و کلاسهای زیر را به پروژه اضافه کنید:
public class CustomUserValidator<TUser, TKey> : IIdentityValidator<ApplicationUser> where TUser : class, IUser<int> where TKey : IEquatable<int> { public bool AllowOnlyAlphanumericUserNames { get; set; } public bool RequireUniqueEmail { get; set; } private ApplicationUserManager Manager { get; set; } public CustomUserValidator(ApplicationUserManager manager) { if (manager == null) throw new ArgumentNullException("manager"); AllowOnlyAlphanumericUserNames = true; Manager = manager; } public virtual async Task<IdentityResult> ValidateAsync(ApplicationUser item) { if (item == null) throw new ArgumentNullException("item"); var errors = new List<string>(); await ValidateUserName(item, errors); if (RequireUniqueEmail) await ValidateEmailAsync(item, errors); return errors.Count <= 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray()); } private async Task ValidateUserName(ApplicationUser user, ICollection<string> errors) { if (string.IsNullOrWhiteSpace(user.UserName)) errors.Add("نام کاربری نباید خالی باشد"); else if (AllowOnlyAlphanumericUserNames && !Regex.IsMatch(user.UserName, "^[A-Za-z0-9@_\\.]+$")) { errors.Add("برای نام کاربری فقط از کاراکترهای مجاز استفاده کنید "); } else { var owner = await Manager.FindByNameAsync(user.UserName); if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id)) errors.Add("این نام کاربری قبلا ثبت شده است"); } } private async Task ValidateEmailAsync(ApplicationUser user, ICollection<string> errors) { var email = await Manager.GetEmailStore().GetEmailAsync(user).WithCurrentCulture(); if (string.IsNullOrWhiteSpace(email)) { errors.Add("وارد کردن ایمیل ضروریست"); } else { try { var m = new MailAddress(email); } catch (FormatException) { errors.Add("ایمیل را به شکل صحیح وارد کنید"); return; } var owner = await Manager.FindByEmailAsync(email); if (owner != null && !EqualityComparer<int>.Default.Equals(owner.Id, user.Id)) errors.Add("این ایمیل قبلا ثبت شده است"); } } }
public class CustomPasswordValidator : IIdentityValidator<string> { #region Properties public int RequiredLength { get; set; } public bool RequireNonLetterOrDigit { get; set; } public bool RequireLowercase { get; set; } public bool RequireUppercase { get; set; } public bool RequireDigit { get; set; } #endregion #region IIdentityValidator public virtual Task<IdentityResult> ValidateAsync(string item) { if (item == null) throw new ArgumentNullException("item"); var list = new List<string>(); if (string.IsNullOrWhiteSpace(item) || item.Length < RequiredLength) list.Add(string.Format("کلمه عبور نباید کمتر از 6 کاراکتر باشد")); if (RequireNonLetterOrDigit && item.All(IsLetterOrDigit)) list.Add("برای امنیت بیشتر از حداقل از یک کارکتر غیر عددی و غیر حرف برای کلمه عبور استفاده کنید"); if (RequireDigit && item.All(c => !IsDigit(c))) list.Add("برای امنیت بیشتر از اعداد هم در کلمه عبور استفاده کنید"); if (RequireLowercase && item.All(c => !IsLower(c))) list.Add("از حروف کوچک نیز برای کلمه عبور استفاده کنید"); if (RequireUppercase && item.All(c => !IsUpper(c))) list.Add("از حروف بزرک نیز برای کلمه عبور استفاده کنید"); return Task.FromResult(list.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(string.Join(" ", list))); } #endregion #region PrivateMethods public virtual bool IsDigit(char c) { if (c >= 48) return c <= 57; return false; } public virtual bool IsLower(char c) { if (c >= 97) return c <= 122; return false; } public virtual bool IsUpper(char c) { if (c >= 65) return c <= 90; return false; } public virtual bool IsLetterOrDigit(char c) { if (!IsUpper(c) && !IsLower(c)) return IsDigit(c); return true; } #endregion }
سپس باید کلاسهای فوق را به Identity معرفی کنید تا از این کلاسهای سفارشی شده به جای کلاسهای پیش فرض خودش استفاده کند. برای این کار وارد کلاس ApplicationUserManager شده و درون متد createApplicationUserManager کدهای زیر را اضافه کنید:
UserValidator = new CustomUserValidator< ApplicationUser, int>(this) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; PasswordValidator = new CustomPasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = false, RequireUppercase = false };
ایجاد SecurityStamp برای کاربران فعلی سایت
سیستم Identity برای لحاظ کردن یک سری موارد امنیتی، به ازای هر کاربر، فیلدی را به نام SecurityStamp درون دیتابیس ذخیره میکند و برای این که این سیستم عملکرد صحیحی داشته باشد، باید این مقدار را برای کاربران فعلی سایت ایجاد کرد تا کاربران فعلی بتوانند از امکانات Identity نظیر فراموشی کلمه عبور، ورود به سیستم و ... استفاده کنند.
برای این کار Identity، متدی به نام UpdateSecurityStamp را در اختیار قرار میدهد تا با استفاده از آن بتوان مقدار فیلد SecurityStamp را به روز رسانی کرد.
معمولا برای انجام این کارها میتوانید یک کنترلر تعریف کنید و درون اکشن متد آن کلیهی کاربران را واکشی کرده و سپس متد UpdateSecurityStamp را بر روی آنها فراخوانی کنید.
public virtual async Task<ActionResult> UpdateAllUsersSecurityStamp() { foreach (var user in await _userManager.GetAllUsersAsync()) { await _userManager.UpdateSecurityStampAsync(user.Id); } return Content("ok"); }
انتقال نقشهای کاربران به جدول جدید و برقراری رابطه بین آنها
در سیستم Iris رابطهی بین کاربران و نقشها یک به چند بود. در سیستم Identity این رابطه چند به چند است و من به عنوان یک حرکت خوب و رو به جلو، رابطهی چند به چند را در سیستم جدید انتخاب کردم. اکنون با استفاده از دستورات زیر به راحتی میتوان نقشهای فعلی و رابطهی بین آنها را به جداول جدیدشان منتقل کرد:
public virtual async Task<ActionResult> CopyRoleToNewTable() { var dbContext = new IrisDbContext(); foreach (var role in await dbContext.Roles.ToListAsync()) { await _roleManager.CreateAsync(new CustomRole(role.Name) { Description = role.Description }); } var users = await dbContext.Users.Include(u => u.Role).ToListAsync(); foreach (var user in users) { await _userManager.AddToRoleAsync(user.Id, user.Role.Name); } return Content("ok"); }
Gulp #3
نصب bower
- ساده کردن تعریف وابستگیهای منابع پروژه با تعریف یک فایل bower.json
- نیازی به commit کردن واستگیهای پروژه نیست.
- با ذکر ورژن مربوط به وابستگی یا محدودهی قابل قبول برای آن، به روز رسانی منابع به سادگی با یک دستور انجام میشود.
- وابستگی های وابسته به یک منبع را نیز نصب میکند. برای مثال زمانیکه بوت استرپ را به عنوان وابستگی پروژه تعریف میکنیم، وابستگی آن یعنی jquery را چون در فایل bower.json بوت استرپ تعریف شدهاست، به صورت خودکار دانلود میکند.
- در نهایت افراد هم تیمی یا توسعه دهندگان دیگر به راحتی با زدن دستور bower install تمام وابستگیهای پروژه را میتوانند نصب کنند.
sudo npm install -g bower bower init
bower install bootstrap-sass-official --save bower install fontawesome --save bower install bootstrap-rtl --save
نصب پلاگینهای مورد نیاز gulp
sudo npm install gulp gulp-ruby-sass gulp-notify gulp-bower --save-dev
نوشتن تسکها برای گالپ
var gulp = require('gulp'), sass = require('gulp-ruby-sass'), notify = require('gulp-notify'), bower = require('gulp-bower');
var config = { sassPath = './resources/sass', bowerDir = './bower_components' }
// create a task to do bower install gulp.task('bower', function() { return bower() .pipe(gulp.dest(config.bowerDir)) });
// Copy js files to public folder gulp.task('js', function() { return gulp.src([config.bowerDir + '/bootstrap-sass-official/assets/javascripts/bootstrap.min.js', config.bowerDir + '/jquery/dist/jquery.min.js' ]) .pipe(gulp.dest('./public/js')); }); // Copy fontawesome icons to public/fonts folder gulp.task('icons', function() { return gulp.src(config.bowerDir + '/fontawesome/fonts/**.*') .pipe(gulp.dest('./public/fonts')); });
gulp.task('css', function() { return sass(config.sassPath + '/style.scss', { // Our coustom sass style: 'compressed', // minify css loadPath: [ // load paths to easy use import in resources/sass './resources/sass', config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files config.bowerDir + '/fontawesome/scss' // awesome icons sass files ] }) });
gulp.task('css', function() { return sass(config.sassPath + '/style.scss', { // Our coustom sass style: 'compressed', // minify css loadPath: [ // load paths to easy use import in resources/sass './resources/sass', config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files config.bowerDir + '/fontawesome/scss' // awesome icons sass files ] }) .on('error', notify.onError(function(error) { return 'Error: ' + error.message; })) .pipe(gulp.dest('./public/css')); });
// Rerun the task when a file changes gulp.task('watch', function() { gulp.watch(config.sassPath + '/**/*.scss', ['css']); });
// Run this task with : gulp // OR gulp default gulp.task('default', ['bower', 'icons', 'css','js']);
مخزن گیت هاب
UI-Router ابزاری برای مسیریابی در AngularJS است که این امکان را برایتان فراهم میکند تا بخشهای برنامه رابط کاربریتان را به شکل یک ماشین حالت ساماندهی کنید. برخلاف سرویس route$ که بر اساس مسیریابی URLها ساماندهی شده و کار میکند، UI-Router بر اساس حالتها کار میکند، که این حالتها میتوانند در صورت لزوم مسیریابی هم داشته باشند.
UI-Router یکی از افزونههای مجموعه Angular-ui، و پاراگراف بالا معرفی آن در صفحه خانگیش است (تقریبا!). این افزونه جزئیات مفصلی دارد و در این مطلب تنها به معرفی آن خواهم پرداخت (بر اساس مطالب صفحه خانگیش). پیش از ادامه پیشنهاد میکنم اگر مطالب زیر را نخواندهاید ابتدا آنها را مرور کنید:
برای استفاده از UI-Router باید:
- فایل جاوا اسکریپت آن را دانلود کنید (released یا minified).
- در صفحه اصلی برنامهتان پس از include کردن فایل اصلی AngularJS فایل angular-ui-router.js (یا angular-ui-router.min.js) را include کنید.
- 'ui.router' را به لیست وابستگیهای ماژول اصلی اضافه کنید.
<!doctype html> <html ng-app="myApp"> <head> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script> <script src="js/angular-ui-router.min.js"></script> <script> var myApp = angular.module('myApp', ['ui.router']); // For Component users, it should look like this: // var myApp = angular.module('myApp', [require('angular-ui-router')]); </script> ... </head> <body> ... </body> </html>
حالتها و viewهای تو در تو
قابلیت اصلی UI-Router امکان تعریف حالتها و vieweهای تو در تو است. در مطلب مسیریابی در AngularJs #بخش اول دایرکتیو ng-view معرفی شده است. هنگام استفاده از سرویس route$ با این دایرکتیو میتوان محل مورد نظر برای بارگذاری محتویات مربوط به مسیرها را مشخص کرد. دایرکتیو ui-view در UI-Router همین نقش را دارد. فرض کنید این کد فایل index.html باشد:<!-- index.html --> <body> <div ui-view></div> <!-- We'll also add some navigation: --> <a ui-sref="state1">State 1</a> <a ui-sref="state2">State 2</a> </body>
در ادامه برای هر کدام از حالتها یک template اضافه میکنیم:
فایل state1.html:
<!-- partials/state1.html --> <h1>State 1</h1> <hr/> <a ui-sref="state1.list">Show List</a> <div ui-view></div>
<!-- partials/state2.html --> <h1>State 2</h1> <hr /> <a ui-sref="state2.list">Show List</a> <div ui-view></div>
دو نکته قابل توجه در این templateها وجود دارد. اول اینکه همانطور که میبینید templateها خود شامل تگی با دایرکتیو ui-view هستند. و دوم مقدار دایرکتیو ui-sref است که به صورت state1.list و state2.list آمده است. این جدا سازی با نقطه نشان دهنده سلسله مراتب حالتهاست. یعنی حالتهای state1 و state2 هرکدام حالت فرزندی به نام list دارند. در ادامه وقتی حالتها و مسیریابی را در ()app.config تعریف کنیم این مسائل از هالهای از ابهام که در آن هستند خارج میشوند! فعلا بیاید با راهنمای UI-Router پیش برویم و فایلهای template حالتهای فرزند را تعریف کنیم. templateهایی که قرار است در ui-view پدرانشان بارگذاری شوند:
<!-- partials/state1.list.html --> <h3>List of State 1 Items</h3> <ul> <li ng-repeat="item in items">{{ item }}</li> </ul>
<!-- partials/state2.list.html --> <h3>List of State 2 Things</h3> <ul> <li ng-repeat="thing in things">{{ thing }}</li> </ul>
myApp.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { // // For any unmatched url, redirect to /state1 $urlRouterProvider.otherwise("/state1"); // // Now set up the states $stateProvider .state('state1', { url: "/state1", templateUrl: "partials/state1.html" }) .state('state1.list', { url: "/list", templateUrl: "partials/state1.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } }) .state('state2', { url: "/state2", templateUrl: "partials/state2.html" }) .state('state2.list', { url: "/list", templateUrl: "partials/state2.list.html", controller: function($scope) { $scope.things = ["A", "Set", "Of", "Things"]; } }) }]);
خصوصیت url مشخص کننده مسیر حالت است. این خصوصیت همان مقداریست که به عنوان پارامتر اول به ()routeProvider.when$ پاس میشد. در این پارامتر میشود متغیرهای url را هم به همان ترتیب تعریف کرد. مثلا اگر حالت state1 در آدرسش یک پارامتر id داشته باشد میشود آن را به این ترتیب تعریف کرد:
.state('state1', { url: "/state1/:id", templateUrl: "partials/state1.html" })
$stateParams.id
.state('list', { parent: "state1", url: "/list", templateUrl: "partials/state1.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } }) .state('list', { parent: "state2", url: "/list", templateUrl: "partials/state2.list.html", controller: function($scope) { $scope.items = ["A", "List", "Of", "Items"]; } })
- وابستگیهای فراهم شده در حالت پدر به وسیله "resolve"
- دادههای سفارشی مشخص شده در خصوصیت data حالت پدر
.state('state1', { url: "/state1", templateUrl: "partials/state1.html", data:{ foodata: 'addorder' } })
$state.current.data.foodata
Viewهای نامگذاری شده و چندگانه
<!-- index.html --> <body> <div ui-view="viewA"></div> <div ui-view="viewB"></div> <!-- Also a way to navigate --> <a ui-sref="route1">Route 1</a> <a ui-sref="route2">Route 2</a> </body>
myApp.config(function ($stateProvider) { $stateProvider .state('index', { url: "", views: { "viewA": { template: "index.viewA" }, "viewB": { template: "index.viewB" } } }) .state('route1', { url: "/route1", views: { "viewA": { template: "route1.viewA" }, "viewB": { template: "route1.viewB" } } }) .state('route2', { url: "/route2", views: { "viewA": { template: "route2.viewA" }, "viewB": { template: "route2.viewB" } } }) });
چند نکته
.state('list', { parent: "state1", url: "/list", templateUrl: "partials/state1.list.html", controller: "state1ListController as listCtrl1" } }) .state('list', { parent: "state2", url: "/list", templateUrl: "partials/state2.list.html", controller: "state2ListController as listCtrl2" } })
حالتهای انتزاعی
حساسیت به حروف بزرگ و کوچک
- در فایل "angular-ui-router.js" عبارت "new RegExp(compiled)" را پیدا کرده و آن را به "RegExp(compiled, 'i')" تبدیل کنید. و یا در "angular-ui-router.min.js" (هرکدام از فایلها که استفاده میکنید) عبارت new RegExp(o) را پیدا کرده و آن را به new RegExp(o, "i") تبدیل کنید. همین؛ صدایش را هم در نیاورید!
بوت استرپ (نگارش 3) چیست؟
میخواستم بدونم چرا به جای خواندن جی کوئری از سایت آن
< script src = "http://code.jquery.com/jquery-latest.min.js" ></ script >
< script src = "Scripts/jquery-latest.min.js " ></ script >