var department = (from d in context.Departments where d.Name == DepartmentNames.English select d).FirstOrDefault();
تعریف مدل سمت کاربر برنامه
فایل جدید Scripts\App\store.js را اضافه کرده و محتوای آنرا به نحو ذیل تغییر دهید:
var posts = [ { id: '1', title: "Getting Started with Ember.js", body: "Bla bla bla 1." }, { id: '2', title: "Routes and Templates", body: "Bla bla bla 2." }, { id: '3', title: "Controllers", body: "Bla bla bla 3." } ]; var comments = [ { id: '1', postId: '3', text: 'Thanks!' }, { id: '2', postId: '3', text: 'Good to know that!' }, { id: '3', postId: '1', text: 'Great!' } ];
سپس جهت استفاده از آن، تعریف مدخل آنرا به فایل index.html، پیش از تعاریف کنترلرها اضافه خواهیم کرد:
<script src="Scripts/App/store.js" type="text/javascript"></script>
ویرایش قالب مطالب برای نمایش لیستی از عناوین ارسالی
قالب فعلی Scripts\Templates\posts.hbs صرفا دارای یک سری عنوان درج شده به صورت مستقیم در صفحه است. اکنون قصد داریم آنرا جهت نمایش لیستی از آرایه مطالب تغییر دهیم.
همانطور که در تصویر ملاحظه میکنید، با درخواست آدرس صفحهی مطالب، router آن مسیریابی متناظری را یافته و سپس بر این اساس، template، کنترلر و مدلی را انتخاب میکند. به صورت پیش فرض، قالب و کنترلر انتخاب شده، مواردی هستند همنام با مسیریابی جاری. اما مقدار پیش فرضی برای model وجود ندارد و باید آنرا به صورت دستی مشخص کرد.
برای این منظور فایل Scripts\Routes\posts.js را به پوشهی routes با محتوای ذیل اضافه کنید:
Blogger.PostsRoute = Ember.Route.extend({ controllerName: 'posts', renderTemplare: function () { this.render('posts'); }, model: function () { return posts; } });
همچنین اگر به خاطر داشته باشید، در پوشهی کنترلرها فایل posts.js تعریف نشدهاست. اگر اینکار صورت نگیرد، ember.js به صورت خودکار کنترلر پیش فرضی را ایجاد خواهد کرد. در کل، یک قالب هیچگاه به صورت مستقیم با مدل کار نمیکند. این کنترلر است که مدل را در اختیار یک قالب قرار میدهد.
سپس مدخل تعریف این فایل را به فایل index.html، پس از تعاریف کنترلرها اضافه نمائید:
<script src="Scripts/Routes/posts.js" type="text/javascript"></script>
اکنون فایل Scripts\Templates\posts.hbs را گشوده و به نحو ذیل، جهت نمایش عناوین مطالب، ویرایش کنید:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{post.title}}</li> {{/each}} </ul>
نمایش لیست آخرین نظرات ارسالی
در ادامه قصد داریم تا آرایه comments ابتدای بحث را در صفحهای جدید نمایش دهیم. بنابراین نیاز است تا ابتدا مسیریابی آن تعریف شود. بنابراین فایل Scripts\App\router.js را گشوده و مسیریابی جدید recent-comments را به آن اضافه کنید:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); });
Blogger.RecentCommentsRoute = Ember.Route.extend({ model: function () { return comments; } });
همچنین نیاز است تا تعریف مدخل این فایل جدید را نیز به انتهای تعاریف مداخل فایل index.html اضافه کنیم:
<script src="Scripts/Routes/recent-comments.js" type="text/javascript"></script>
اکنون قالب application واقع در فایل Scripts\Templates\application.hbs را جهت افزودن منوی مرتبط با این مسیریابی جدید، به نحو ذیل ویرایش خواهیم کرد:
<div class='container'> <nav class='navbar navbar-default' role='navigation'> <ul class='nav navbar-nav'> <li>{{#link-to 'posts'}}Posts{{/link-to}}</li> <li>{{#link-to 'recent-comments'}}Recent comments{{/link-to}}</li> <li>{{#link-to 'about'}}About{{/link-to}}</li> <li>{{#link-to 'contact'}}Contact{{/link-to}}</li> </ul> </nav> {{outlet}} </div>
<h1>Recent comments</h1> <ul> {{#each comment in model}} <li>{{comment.text}}</li> {{/each}} </ul>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments' ]); </script>
نمایش مجزای هر مطلب در یک صفحهی جدید
تا اینجا در صفحهی اول سایت، لیست عناوین مطالب را نمایش دادیم. در ادامه نیاز است تا بتوان هر عنوان را به صفحهی متناظر و اختصاصی آن لینک کرد؛ برای مثال لینکی مانند http://localhost:25918/#/posts/3 به سومین مطلب ارسالی اشاره میکند. Ember.js به عدد 3 در اینجا، یک dynamic segment میگوید. از این جهت که مقدار آن بر اساس شماره مطلب درخواستی، متفاوت خواهد بود. برای پردازش این نوع آدرسها نیاز است مسیریابی ویژهای را تعریف کرد. فایل Scripts\App\router.js را گشوده و سپس مسیریابی post را به نحو ذیل به آن اضافه نمائید:
Blogger.Router.map(function () { this.resource('posts', { path: '/' }); this.resource('about'); this.resource('contact', function () { this.resource('email'); this.resource('phone'); }); this.resource('recent-comments'); this.resource('post', { path: 'posts/:post_id' }); });
با توجه به اینکه این مسیریابی جدید post نام گرفت (جهت نمایش یک مطلب)، به صورت خودکار، کنترلر و قالبی به همین نام را بارگذاری میکند. همچنین مدل خود را نیز باید از مسیریابی خاص خود دریافت کند. بنابراین فایل جدید Scripts\Routes\post.js را در پوشهی routes با محتوای ذیل اضافه کنید:
Blogger.PostRoute = Ember.Route.extend({ model: function (params) { return posts.findBy('id', params.post_id); } });
برای مثال، جهت آدرس http://localhost:25918/#/posts/3، مقدار post_id به صورت خودکار به عدد 3 تنظیم میشود.
پس از آن نیاز است مدخل این فایل جدید را در صفحهی index.html نیز اضافه کنیم:
<script src="Scripts/Routes/post.js" type="text/javascript"></script>
در ادامه برای نمایش اطلاعات مدل نیاز است قالب جدید Scripts\Templates\post.hbs را با محتوای زیر اضافه کنیم:
<h1>{{title}}</h1> <p>{{body}}</p>
<script type="text/javascript"> EmberHandlebarsLoader.loadTemplates([ 'posts', 'about', 'application', 'contact', 'email', 'phone', 'recent-comments', 'post' ]); </script>
اکنون به قالب Scripts\Templates\posts.hbs مراجعه کرده و هر عنوان را به مطلب متناظر با آن لینک میکنیم:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul>
همچنین با کلیک بر روی هر عنوان نیز مطلب مرتبط نمایش داده خواهد شد:
افزودن امکان ویرایش مطالب
میخواهیم در صفحهی نمایش جزئیات یک مطلب، امکان ویرایش آنرا نیز فراهم کنیم. بنابراین فایل Scripts\Templates\post.hbs را گشوده و محتوای آنرا به نحو ذیل ویرایش کنید:
<h2>{{title}}</h2> {{#if isEditing}} <form> <div class="form-group"> <label for="title">Title</label> {{input value=title id="title" class="form-control"}} </div> <div class="form-group"> <label for="body">Body</label> {{textarea value=body id="body" class="form-control" rows="5"}} </div> <button class="btn btn-primary" {{action 'save' }}>Save</button> </form> {{else}} <p>{{body}}</p> <button class="btn btn-primary" {{action 'edit' }}>Edit</button> {{/if}}
در فرم تعریف شده، المانهای ورودی اطلاعات از handlebar helperهای ویژهی input و textarea استفاده میکنند؛ بجای المانهای متداول HTML. همچنین value یکی به title و دیگری به body تنظیم شدهاست (خواص مدل ارائه شده توسط کنترلر متصل به قالب). این مقادیر نیز داخل '' قرار ندارند؛ به عبارتی در یک handlebar helper به عنوان متغیر در نظر گرفته میشوند. به این ترتیب اطلاعات کنترلر جاری، به این المانهای ورودی اطلاعات به صورت خودکار bind میشوند و برعکس. اگر کاربر مقادیر آنها را تغییر دهد، تغییرات نهایی به صورت خودکار به خواص متناظری در کنترلر جاری منعکس خواهند شد (two-way data binding).
دو دکمه نیز تعریف شدهاند که به اکشنهای save و edit متصل هستند.
بنابراین نیاز به یک کنترلر جدید، به نام post داریم تا بتوان رفتار قالب post را کنترل کرد. برای این منظور فایل جدید Scripts\Controllers\post.js را با محتوای ذیل ایجاد کنید:
Blogger.PostController = Ember.ObjectController.extend({ isEditing: false, actions: { edit: function () { this.set('isEditing', true); }, save: function () { this.set('isEditing', false); } } });
<script src="Scripts/Controllers/post.js" type="text/javascript"></script>
اگر به کدهای این کنترلر دقت کرده باشید، اینبار زیرکلاسی از ObjectController ایجاد شدهاست و نه Controller، مانند مثالهای قبل. ObjectController تغییرات رخ داده بر روی خواص مدل را که توسط کنترلر در معرض دید قالب قرار دادهاست، به صورت خودکار به مدل مرتبط نیز منعکس میکند (Ember.ObjectController.extend)؛ اما Controller خیر (Ember.Controller.extend). در اینجا مدل کنترلر، تنها «یک» شیء است که بر اساس id آن انتخاب شدهاست. به همین جهت از ObjectController برای ارائه two-way data binding کمک گرفته شد.
در ember.js، یک قالب تنها با کنترلر خودش دارای تبادل اطلاعات است. اگر این کنترلر از نوع ObjectController باشد، تغییرات خاصیتی در یک قالب، ابتدا به کنترلر آن منعکس میشود و سپس این کنترلر، در صورت یافتن معادلی از این خاصیت در مدل، آنرا به روز خواهد کرد. در حالت استفاده از Controller معمولی، صرفا تبادل اطلاعات بین قالب و کنترلر را شاهد خواهیم بود و نه بیشتر.
در ابتدای کار مقدار خاصیت isEditing مساوی false است. این مورد سبب میشود تا در بار اول بارگذاری اطلاعات یک مطلب انتخابی، صرفا عنوان و محتوای مطلب نمایش داده شوند؛ به همراه یک دکمهی edit. با کلیک بر روی دکمهی edit، مطابق کدهای کنترلر فوق، تنها خاصیت isEditing به true تنظیم میشود و در این حالت، بدنهی اصلی شرط if isEditing در قالب post، رندر خواهد شد.
برای مثال در ابتدا مطلب شماره یک را انتخاب میکنیم:
با کلیک بر روی دکمهی edit، فرم ویرایش ظاهر خواهد شد:
نکتهی جالب آن، مقدار دهی خودکار المانهای ویرایش اطلاعات است. در این حالت سعی کنید، عنوان مطلب جاری را اندکی ویرایش کنید:
با ویرایش عنوان، میتوان بلافاصله مقدار تغییر یافته را در برچسب عنوان مطلب نیز مشاهده کرد. این مورد دقیقا مفهوم two-way data binding و اتصال مقادیر value هر کدام از handlebar helperهای ویژهی input و textarea را به عناصر مدل ارائه شده توسط کنترلر post، بیان میکند.
در این حالت در کدهای متد save، تنها کافی است که خاصیت isEditing را به false تنظیم کنیم. زیرا کلیه مقادیر ویرایش شده توسط کاربر، در همان لحظه در برنامه منتشر شدهاند و نیاز به کار بیشتری برای اعمال تغییرات نیست.
اضافه کردن دکمهی مرتب سازی بر اساس عناوین، در صفحهی اول سایت
Ember.ObjectController.extend برای data bindg یک شیء کاربرد دارد. اگر قصد داشته باشیم با آرایهای از اشیاء کار کنیم میتوان از ArrayController استفاده کرد. فرض کنید در صفحهی اول سایت میخواهیم امکان مرتب سازی مطالب را بر اساس عنوان آنها اضافه کنیم. فایل Scripts\Templates\posts.hbs را گشوده و لینک Sort by title را به انتهای آن اضافه کنید:
<h2>Emeber.js blog</h2> <ul> {{#each post in model}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
Blogger.PostsController = Ember.ArrayController.extend({ sortProperties: ['id'],// مقادیر پیش فرض مرتب سازی sortAscending: false, actions: { sortByTitle: function () { this.set('sortProperties', ['title']); this.set('sortAscending', !this.get('sortAscending')); } } });
در ادامه، تعریف مدخل این کنترلر جدید را نیز باید به فایل index.html، اضافه کرد:
<script src="Scripts/Controllers/posts.js" type="text/javascript"></script>
اگر برنامه را در این حالت اجرا کرده و بر روی دکمهی Sort by title کلیک کنید، اتفاقی رخ نمیدهد. علت اینجا است که ArrayController خروجی تغییر یافته خودش را توسط خاصیتی به نام arrangedContent در اختیار قالب خود قرار میدهد. بنابراین نیاز است فایل قالب Scripts\Templates\posts.hbs را به نحو ذیل ویرایش کرد:
<h2>Emeber.js blog</h2> <ul> {{#each post in arrangedContent}} <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
یک نکته: حلقهی ویژهای به نام each
اگر قالب Scripts\Templates\posts.hbs را به نحو ذیل، با یک حلقهی each ساده بازنویسی کنید:
<h2>Ember.js blog</h2> <ul> {{#each}} <li>{{#link-to 'post' id}}{{title}}{{/link-to}}</li> {{/each}} </ul> <a href="#" class="btn btn-primary" {{action 'sortByTitle'}}>Sort by title</a>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
EmberJS03_03.zip
مراحل فعال سازی آپلود فایلها در ASP.NET Core
مرحلهی اول فعال سازی آپلود فایلها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شدهاست:
<form method="post" asp-action="Index" asp-controller="TestFileUpload" enctype="multipart/form-data"> <input type="file" name="files" multiple /> <input type="submit" value="Upload" /> </form>
در سمت سرور، امضای اکشن متد دریافت کنندهی این فایلها به صورت ذیل خواهد بود:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files)
یافتن جایگزینی برای Server.MapPath در ASP.NET Core
زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحلهی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمیدانیم ریشهی سایت در کدام پوشهی سرور واقع شدهاست، میشد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشهی سایت یا همان پوشهی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller { private readonly IHostingEnvironment _environment; public TestFileUploadController(IHostingEnvironment environment) { _environment = environment; }
پس از آن خاصیت environment.WebRootPath_ به ریشهی پوشهی wwwroot برنامه، بر روی سرور اشاره میکند. به این ترتیب میتوان مسیر دقیقی را جهت ذخیره سازی فایلهای رسیده، مشخص کرد.
امکان ذخیره سازی async فایلها در ASP.NET Core
عملیات کار با فایلها، عملیاتی است که از مرزهای IO سیستم عبور میکند. به همین جهت یکی از بهترین مثالهای پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخدهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی میشود و این stream میتواند یک FileStream و یا MemoryStream باشد. در ذیل نحوهی کار async با یک FileStream را مشاهده میکنید:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files) { var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } foreach (var file in files) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream).ConfigureAwait(false); } } return View(); }
چون برنامههای ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکنندهی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام میشود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه میکنید.
پشتیبانی کامل از Model Binding آپلود فایلها در ASP.NET Core
در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمیکند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آنرا به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شدهاند:
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
@model UserViewModel <form method="post" asp-action="UploadPhoto" asp-controller="TestFileUpload" enctype="multipart/form-data"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Photo" /> <span asp-validation-for="Photo" class="text-danger"></span> <input type="submit" value="Upload"/> </form>
اینبار جهت فعال سازی و استفادهی از قابلیتهای Model Binding میتوان از ModelState نیز بهره گرفت:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel) { if (ModelState.IsValid) { var formFile = userViewModel.Photo; if (formFile == null || formFile.Length == 0) { ModelState.AddModelError("", "Uploaded file is empty or null."); return View(viewName: "Index"); } var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } var filePath = Path.Combine(uploadsRootFolder, formFile.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await formFile.CopyToAsync(fileStream).ConfigureAwait(false); } RedirectToAction("Index"); } return View(viewName: "Index"); }
بررسی پسوند فایلهای رسیدهی به سرور
ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیتهایی از نوع IFormFile ندارد:
[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
در ادامه جهت بررسی پسوندهای فایلهای رسیده، میتوان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class UploadFileExtensionsAttribute : ValidationAttribute { private readonly IList<string> _allowedExtensions; public UploadFileExtensionsAttribute(string fileExtensions) { _allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } public override bool IsValid(object value) { var file = value as IFormFile; if (file != null) { return isValidFile(file); } var files = value as IList<IFormFile>; if (files == null) { return false; } foreach (var postedFile in files) { if (!isValidFile(postedFile)) return false; } return true; } private bool isValidFile(IFormFile file) { if (file == null || file.Length == 0) { return false; } var fileExtension = Path.GetExtension(file.FileName); return !string.IsNullOrWhiteSpace(fileExtension) && _allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase)); } }
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] //`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties. //[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
در این قسمت میتوانید هر کدام از موارد را که نیاز دارید، نصب کنید. هر کدام از عنوانها در آینده آموزش داده خواهند شد و پیشنهاد میشود آنها را نصب کنید؛ در غیر این صورت فقط گزینهی اول کافی میباشد.
در صورت کلیک بر روی Options، با صفحهی زیر روبرو میشوید که در آن با انتخاب گزینهی Administrative، میتوانید تنظیمات بیشتری را در زمان نصب، انجام دهید و همچنین با انتخاب All users ، نرم افزار برای تمام کاربران سیستم در دسترس خواهد بود.
بعد از اتمام نصب، فایلهای نرم افزار در آدرس زیر در دسترس میباشند:
%LOCALAPPDATA%\JetBrains\Installations
اکنون اگر Visual studio را باز کنید، Resharper به قسمت Extensionsها اضافه شدهاست که در ادامهی آموزش به بررسی آن میپردازیم.
زبانهای پشتیبانی شده
ریشارپر از زبان های C# , VB.NET , TypeScript , JavaScript , C++ , CSS پشتیبانی میکند.
افزایش سرعت Reshaper
یکی از مشکلاتی که بیشتر افراد بعد از نصب این افزونه دارند، مخصوصا اگر سیستم آنها خیلی قوی نباشد، کند شدن Visual Studio میباشد. برای سریعتر کردن این افزونه راه هایی موجود میباشد که به آنها میپردازیم.
- خود ریشارپر پیشنهادهایی را برای بهبود سرعت میکند که آنها در مسیر زیر در دسترس میباشند:
ReSharper | Options | Environment | Performance Guide
- یکی از امکانات ریشارپر، نشان دادن تمام خطاهای موجود در برنامه به صورت یک لیست میباشد که نام این ویژگی، solution-wide analysis است و البته این امکان باعث سنگینی زیاد Visual studio میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید:
ReSharper | Options | Code Inspection | Settings
- راه دیگر انجام اینکار از قسمت تنظیمات خود Visual studio است. برای این کار به مسیر زیر بروید و گزینههای گفته شده را غیر فعال کنید.
Environment | General
Automatically adjust visual experience based on client performance Enable rich client visual experience
- همچنین این گزینه را نیز فعال کنید تا جلوی لگ در UI گرفته شود.
Use hardware graphics acceleration if available
- اگر پروژهی بزرگی را دارید، میتوانید گزینهی زیر را نیز غیر فعال کنید. البته با غیر فعال کردن این گزینه در صورت کرش نرم افزار، کدهای ذخیره نشده ازدست میروند.
Environment | AutoRecover
Save AutoRecover information
- اگر با تعداد فایلهای زیادی کار میکنید، امکان Track changes باعث کندی برنامه میشود. برای غیر فعال کردن این گزینه، به مسیر زیر بروید.
Text Editor | General
Track changes
- خود Visual studio گزینههایی را مانند خطاها در Scroll Bar نشان میدهد که این امکانات در ریشارپر هم موجود میباشد. برای غیر فعال کردن این امکان در Visual Studio برای جلوگیری از دو بار نشان دادن اطلاعات، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | All Languages | Scroll Bars
Show annotations over vertical scroll bar
- یکی دیگر از امکانات Visual Studio گزینهای به اسم CodeLens می باشد که یکی از کارهای آن، نشان دادن تمام رفرنسهای توابع یک فایل، در بالای تابع میباشد. این امکان نیز باعث کندی بسیار زیاد برنامه میشود. برای غیر فعال کردن آن میتوانید به مسیر زیر بروید.
Text Editor | All Languages | CodeLens
- هم Visual Studio و هم Reshaper کدهای شما را Format میکنند. پس برای جلوگیری از دوبار انجام شدن این کار، به مسیر زیر بروید و گزینهی گفته شده را غیر فعال کنید.
Text Editor | [Language] | Formatting
auto-formatting
- اگر از تمام امکانات Reshaper نمیخواهید استفاده کنید، میتوانید آنها را از آدرس زیر غیر فعال کنید.
Environment | Products & Features
- اگر در زمان تایپ کردن، برنامه کند میباشد، میتوانید بعضی از امکانات Resharper را از آدرس زیر غیر فعال کنید.
Environment | IntelliSense
Completion Appearance ReSharper's IntelliSense for specific languages
امکان تعریف HTML Forms استاندارد در Blazor 8x
فرمهای استاندارد HTML، پیش از ظهور جاوااسکریپت و SPAها وجود داشتند (دقیقا همان زمانیکه که فقط مفهوم SSR وجود خارجی داشت) و هنوز هم جزء مهمی از اغلب برنامههای وب را تشکیل میدهند. با ارائهی دات نت 8 و قابلیت server side rendering آن، کامپوننتهای برنامه، فقط یکبار در سمت سرور رندر شده و HTML سادهی آنها به سمت مرورگر کاربر بازگشت داده میشود. در این حالت، فرمهای استاندارد HTML، امکان دریافت ورودیهای کاربر و ارسال دادههای آنها را به سمت سرور میسر میکنند (چون دیگر خبری از اتصال دائم SignalR نیست و باید اطلاعات را به همان نحو استاندارد پروتکل HTTP، به سمت سرور Post کرد). در دات نت 8، دو راهحل برای کار با فرمها در برنامههای Blazor وجود دارد: استفاده از EditForm خود Blazor و یا استفاده از HTML forms استاندارد و ساده، به همان نحوی که بوده و هست.
روش کار با EditForm در برنامههای Blazor SSR
البته ما قصد استفاده از فرمهای سادهی HTML را در اینجا نداریم و ترجیح میدهیم که از همان EditForm استفاده کنیم. EditForms در Blazor بسیار مفید بوده و امکان بایند خواص یک مدل را به اجزای مختلف ورودیهای تعریف شدهی در آن میسر میکند و همچنین قابلیتهایی مانند اعتبارسنجی و امثال آنرا نیز به همراه دارد (اطلاعات بیشتر). اما چگونه میتوان از این امکان در برنامههای Blazor SSR نیز استفاده کرد؟
برای این منظور، ابتدا مثالی را به صورت زیر تکمیل میکنیم (که بر اساس قالب dotnet new blazor --interactivity Server تهیه شده) و سپس توضیحات آن ارائه خواهد شد:
الف) تهیه یک مدل برای تعریف محلهای مرتبط با یک سفارش در فایل Models/OrderPlace.cs
using System.ComponentModel.DataAnnotations; namespace Models; public record OrderPlace { public Address BillingAddress { get; set; } = new(); public Address ShippingAddress { get; set; } = new(); } public class Address { [Required] public string Name { get; set; } = default!; public string? AddressLine1 { get; set; } public string? AddressLine2 { get; set; } public string? City { get; set; } [Required] public string PostCode { get; set; } = default!; }
ب) تهیهی یک کامپوننت Editor برای دریافت اطلاعات آدرس فوق در فایل Components\Pages\Chekout\AddressEntry.razor
@inherits Editor<Models.Address> <div> <label>Name</label> <InputText @bind-Value="Value.Name"/> </div> <div> <label>Address 1</label> <InputText @bind-Value="Value.AddressLine1"/> </div> <div> <label>Address 2</label> <InputText @bind-Value="Value.AddressLine2"/> </div> <div> <label>City</label> <InputText @bind-Value="Value.City"/> </div> <div> <label>Post Code</label> <InputText @bind-Value="Value.PostCode"/> </div>
ج) استفاده از مدل و ادیتور فوق در یک EditForm تغییر یافته برای کار با برنامههای Blazor SSR در فایل Components\Pages\Chekout\Checkout.razor
@page "/checkout" @using Models @if (!_submitted && PlaceModel != null) { <EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout"> <DataAnnotationsValidator/> <h4>Bill To:</h4> <AddressEntry @bind-Value="PlaceModel.BillingAddress"/> <h4>Ship To:</h4> <AddressEntry @bind-Value="PlaceModel.ShippingAddress"/> <button type="submit">Submit</button> <ValidationSummary/> </EditForm> } @if (_submitted && PlaceModel != null) { <div> <h2>Order Summary</h2> <h3>Shipping To:</h3> <dl> <dt>Name</dt> <dd>@PlaceModel.BillingAddress.Name</dd> <dt>Address 1</dt> <dd>@PlaceModel.BillingAddress.AddressLine1</dd> <dt>Address 2</dt> <dd>@PlaceModel.BillingAddress.AddressLine2</dd> <dt>City</dt> <dd>@PlaceModel.BillingAddress.City</dd> <dt>Post Code</dt> <dd>@PlaceModel.BillingAddress.PostCode</dd> </dl> </div> } @code { bool _submitted; [SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; } protected override void OnInitialized() { PlaceModel ??= GetOrderPlace(); } private void SubmitOrder() { _submitted = true; } private static OrderPlace GetOrderPlace() => new() { BillingAddress = new Address { PostCode = "12345", Name = "Test 1", }, ShippingAddress = new Address { PostCode = "67890", Name = "Test 2", }, }; }
باید بخاطر داشت که این فرم بر اساس حالت Server Side Rendering در اختیار مرورگر کاربر قرار میگیرد. یعنی برای بار اول، یک HTML خالص، در سمت سرور بر اساس اطلاعات آن تهیه شده و بازگشت داده میشود و زمانیکه به کاربر نمایش داده شد، دیگر برخلاف Blazor Server پیشین، اتصال SignalR ای وجود ندارد تا قابلیتهای تعاملی آنرا مدیریت کند. در این حالت اگر به view source صفحهی جاری رجوع کنیم، چنین خروجی قابل مشاهدهاست:
<form method="post"> <input type="hidden" name="_handler" value="checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxx" /> . . . <button type="submit">Submit</button> </form>
این EditForm تعریف شده، دو قسمت اضافهتر را نسبت به EditFormهای نگارشهای قبلی Blazor دارد:
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout">
همانطور که در نحوهی تعریف فرم HTML ای فوق مشخص است، فیلد مخفی handler_، کار متمایز ساختن این فرم را به عهده داشته و از مقدار آن در سمت سرور جهت یافتن کامپوننت متناظر، استفاده خواهد شد.
همچنین برای دریافت و پردازش این اطلاعات در سمت سرور، تنها کافی است خاصیت مرتبط با آنرا با ویژگی SupplyParameterFromForm مزین کنیم:
[SupplyParameterFromForm] public OrderPlace? PlaceModel { get; set; }
جریان کاری این فرم به صورت خلاصه به نحو زیر است (که در آن متد OnInitialized دوبار فراخوانی میشود و باید به آن دقت داشت):
- در بار اول نمایش این صفحه (با فراخوانی مسیر /checkout در مرورگر)، متد OnInitialized فراخوانی شده و در آن، مقدار شیء PlaceModel نال است.
- بنابراین به متد GetOrderPlace مراجعه کرده و اطلاعاتی را دریافت میکند؛ برای مثال، این اطلاعات را از سرویسی میخواند.
- پس از پایان هر روال رخدادگردانی در Blazor، در پشت صحنه به صورت خودکار، متد تغییر حالت جاری کامپوننت (متد StateHasChanged) هم فراخوانی میشود. این فراخوانی خودکار، باعث رندر مجدد UI آن بر اساس اطلاعات جدید خواهد شد. یعنی قسمتهای نمایش فرم و نمایش اطلاعات ارسالی، یکبار ارزیابی شده و در صورت برقراری شرطها، نمایش داده میشوند.
- در ادامه، کاربر فرم را پر کرده و به سمت سرور POST میکند.
- پیش از هر رخدادی، خواص شیء PlaceModel به علت مزین بودن به ویژگی SupplyParameterFromForm، بر اساس اطلاعات ارسالی به سرور، مقدار دهی میشوند.
- سپس متد OnInitialized فراخوانی شده و چون اینبار مقدار PlaceModel نال نیست، به متد GetOrderPlace جهت دریافت مقادیر ابتدایی خود مراجعه نمیکند. سطر تعریف شدهی در متد OnInitialized فقط زمانی سبب مقدار دهی شیء PlaceModel میشود که مقدار این شیء، نال باشد (یعنی فقط در اولین بار نمایش صفحه)؛ اما اگر این مقدار توسط پارامتر مزین شدهی به SupplyParameterFromForm به علت ارسال دادههای فرم به سرور، مقدار دهی شده باشد، دیگر به منبع دادهی ابتدایی رجوع نمیکند.
- چون متد رخدادگردان OnInitialized فراخوانی شده، پس از پایان آن (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر کار رندر UI فرم جاری بر اساس اطلاعات جدید، انجام خواهد شد.
- اکنون است که پس از طی این رخدادها، متد رویدادگردان SubmitOrder فراخوانی میشود. یعنی زمانیکه این متد فراخوانی میشود، شیء PlaceModel بر اساس اطلاعات رسیدهی از طرف کاربر، مقدار دهی شده و آمادهی استفاده است (برای مثال آمادهی ذخیره سازی در بانک اطلاعاتی؛ با فراخوانی سرویسی در اینجا).
- پس از پایان فراخوانی متد رویدادگردان SubmitOrder، به علت تغییر حالت کامپوننت (و فراخوانی خودکار متد StateHasChanged در انتهای آن)، یکبار دیگر نیز کار رندر UI فرم جاری بر اساس اطلاعات جدید انجام خواهد شد. یعنی اینبار قسمت Order Summary نمایش داده میشود.
مدیریت تداخل نامهای HTML Forms در Blazor 8x SSR
تمام فرمهایی که به این صورت در برنامههای Blazor SSR مدیریت میشوند، باید دارای نام منحصربفردی که توسط خاصیت FormName مشخص میشود، باشند. برای جلوگیری از این تداخل نامها، کامپوننت جدیدی به نام FormMappingScope معرفی شدهاست که نمونهای از آنرا در فایل فرضی Components\Pages\Chekout\CheckoutForm.razor تعریف شدهی به صورت زیر مشاهده میکنید:
@page "/checkout" <FormMappingScope Name="store-checkout"> <CheckoutForm /> </FormMappingScope>
اکنون اگر برنامه را اجرا کرده و خروجی HTML آنرا بررسی کنیم، به فرم زیر خواهیم رسید:
<form method="post"> <input type="hidden" name="_handler" value="[store-checkout]checkout" /> <input type="hidden" name="__RequestVerificationToken" value="CfDxxxxx" /> . . . <button type="submit">Submit</button> </form>
یک نکته: اگر به تگهای فرم HTML ای فوق دقت کنید، به همراه یک anti-forgery token نیز هست که کار تولید و مدیریت آن، به صورت خودکار صورت میگیرد و میانافزاری نیز برای آن طراحی شده که در فایل Program.cs برنامه، به صورت app.UseAntiforgery بکارگرفته شدهاست.
یک نکته: در Blazor 8x SSR میتوان بجای EditForm، از همان HTML form متداول هم استفاده کرد
اگر بخواهیم بجای استفاده از EditForm، از فرمهای استاندارد HTML هم در حالت SSR استفاده کنیم، این کار میسر بوده و روش کار به صورت زیر است:
<form method="post" @onsubmit="SaveData" @formname="MyFormName"> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form>
پردازش فرمهای GET در Blazor 8x
در حالتیکه از فرمهای استاندارد HTML ای استفاده میشود، ممکن است method فرم، بجای post، حالت get باشد که نتایج آن به صورت کوئری استرینگ در نوار آدرس مرورگر ظاهر میشوند؛ مانند جستجوی گوگل که اشخاص میتوانند کوئری استرینگ و لینک نهایی را به اشتراک بگذارند. روش پردازش یک چنین فرمهایی به صورت زیر است:
@page "/" <form method="GET"> <input type="text" name="q"/> <button type="submit">Search</button> </form> @code { [SupplyParameterFromQuery(Name="q")] public string SearchTerm { get; set; } protected override async Task OnInitializedAsync() { // do something with the search term } }
یک ابتکار! تعاملی کردن قسمتی از صفحه بدون فعالسازی کامل Blazor Server و یا Blazor WASM کامل
این دکمهی قرار گرفتهی در یک صفحهی SSR را ملاحظه کنید:
<button class="nav-link border-0" @onclick="BeginSignOut">Log out</button>
<EditForm Context="ctx" FormName="LogoutForm" method="post" Model="@Foo" OnValidSubmit="BeginSignOut"> <button type="submit" class="nav-link border-0">Log out</button> </EditForm> @code{ [SupplyParameterFromForm(Name = "LogoutForm")] public string? Foo { get; set; } protected override void OnInitialized() => Foo = ""; async Task BeginSignOut() { // TODO: SignOutAsync(); // TODO: NavigateTo("/authentication/logout"); } }
یک نکته: میتوان حالت post-back مانند فرمهای تعاملی Blazor 8x را تغییر داد.
به همراه ویژگیهای جدید مرتبط با صفحات SSR، ویژگی هدایت بهبودیافته هم وجود دارد که جزئیات بیشتر آنرا در قسمتهای بعدی این سری بررسی میکنیم. برای نمونه اگر مثال این قسمت را اجرا کنید، فرم آن به همراه یک post-back مانند به سمت سرور است که کاملا قابل احساس است؛ این رفتار هرچند استاندارد است، اما بیشباهت به برنامههای MVC ، Razor pages و یا وبفرمها نیست و با فرمهای بیصدا و سریع نگارشهای قبلی Blazor متفاوت است. در Blazor8x میتوان این نوع ارسال اطلاعات را Ajax ای هم کرد که به آن enhanced navigation میگویند. برای اینکار فقط کافی است ویژگی Enhance را به تگ EditForm اضافه کرد و یا ویژگی جدید data-enhance را به تگهای فرمهای استاندارد HTML ای افزود. پس از آن اگر برنامه را اجرا کنیم، دیگر یک post-back استاندارد وبفرمها مشاهده نمیشود و رفتار این صفحه بسیار سریع، نرم و روان خواهد بود.
<EditForm Model="PlaceModel" method="post" OnValidSubmit="SubmitOrder" FormName="checkout" Enhance>
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: Blazor8x-Server-Normal.zip
در راستای مهاجرت به ویندوز 7، کار نصب و راه اندازی SVN و کلاینتهای آن باید مجددا انجام میشد. اگر برای بار اول است که به مبحث SVN برخورد میکنید، مطالعه این جزوه توصیه میشود. مطالب ذیل برای افرادی مفید است که قصد انتقال سیستم SVN موجود خود را به مکان و یا سیستم عامل دیگری در اسرع وقت دارند.
الف) دریافت و نصب Visual SVN server
یا میتوان SVN خالص را از سایت آن دریافت کرد و یا جهت سهولت کار و همچنین دسترسی به یک کنسول مدیریتی میتوان برنامهی رایگان Visual SVN server را از آدرس زیر دریافت و نصب کرد:
پس از نصب، ابتدا باید یا کاربر جدیدی را جهت استفاده از منابع آن تعریف کرد و یا از نحوهی اعتبار سنجی یکپارچه با ویندوز هم میتوان استفاده کرد که من از این روش دوم استفاده میکنم (شکل زیر، کلیک راست بر روی نود اصلی visual SVN server و سپس انتخاب خواص و مراجعه به برگهی اعتبار سنجی آن):
ب)دریافت و نصب TortoiseSVN
نصب آن نکتهی خاصی ندارد. اما یک سری نکتهی ریز پس از نصب آن بهتر است رعایت شود که در ادامه ذکر میشود:
ج) دریافت و نصب برنامهی WinMerge
برنامهی Diff پیش فرض TortoiseSVN آنچنان قوی نیست. به همین جهت میتوان برنامهی WinMerge را با آن یکپارچه کرد. برای این منظور ابتدا آنرا دریافت نمائید:
اگر پس از نصب TortoiseSVN آنرا نصب کنید، در حین نصب پیشنهاد یکپارچه سازی با TortoiseSVN را نیز میدهد. اگر ابتدا WinMerge را نصب کردهاید و سپس TortoiseSVN بر روی سیستم شما نصب شده، فقط کافی است مطابق شکل زیر ابتدا به قسمت Diff viewers آن مراجعه کرده و سپس با انتخاب گزینهی external ، دستور خط فرمان زیر را وارد نمائید:
بدیهی است مسیر WinMergeU.exe مطابق مسیر نصب در سیستم شما باید تنظیم شود.
د) تنظیم مسیر تحت نظر قرار گرفتن سیستم
TortoiseSVN به صورت پیش فرض کل سیستم را جهت مشاهدهی تغییرات تحت نظر قرار میدهد که گاهی باعث کاهش کارآیی آن خواهد شد. برای رفع این مشکل میتوان مسیرهایی را که پروژههای شما در آن قرار دارند را به آن معرفی نمود تا بار کلی سیستم کاهش یابد.
همانطور که در شکل نیز ملاحظه میکنید، Include path مقدار دهی شده است.
ه) مشخص سازی پسوندهایی که بهتر است از آنها صرفنظر شود
به برگهی general تنظیمات TortoiseSVN مراجعه کرده و در قسمت global ignore pattern آن، موارد زیر را وارد نمائید:
این موارد شامل پروژههای دات نت، دلفی ، VC و امثال آن است و همچنین یک سری فایل بایناری که عموما با پروژههای برنامه نویسی نیازی به ثبت نگارش آنها نیست.
در همین برگه، اگر هنوز از VS2003 استفاده میکنید، تیک مربوط به استفاده از _svn بجای .svn را قرار دهید تا VS.Net با پوشههای مدیریتی ذکر شده مشکل پیدا نکند.
و) نصب افزونههای SVN سازگار با VS.Net
یا میتوان از افزونهی Visual SVN استفاده کرد (که رایگان نیست) و یا AnkhSVN که رایگان و سورس باز است.
ولی در کل یک مورد را بیشتر نصب نکنید. علت هم کند شدن VS.Net است به دلیل فعالیتهای پشت صحنهی هر کدام از این افزونهها که زیاده روی در تعداد آنها گاها باعث کرش هم میشود. بنابراین همان یک مورد کافی است.
ز) Import مخزنهای قبلی
تا اینجا مقدمات کار فراهم شد. اکنون نوبت به import مخزنهای بجا مانده از سیستم قبلی است. برای اینکار مطابق شکل زیر، گزینهی import existing repositories را انتخاب کرده و مسیر مخزنهای قبلی خود را باید معرفی نمود (به ازای هر کدام یکبار باید این عملیات صورت گیرد).
پس از انجام این مراحل یکبار باید سیستم reboot شود و اکنون همه چیز مثل قبل خواهد شد!
نکته:
اگر مسیر ریشه مخزنهای جدید با مسیر آنها در سیستم قبلی متفاوت است، هنگام commit کارهای خود با خطای زیر متوقف خواهید شد:
Unable to open repository 'file:///C:/Repositories/tracking/trunk'
اشکالی ندارد! برای رفع آن باید از گزینهی relocate مربوط به TortoiseSVN استفاده کرد.
بر روی پوشه کاری پروژه خود کلیک راست کرده، انتخاب گزینهی TortoiseSVN و سپس انتخاب گزینهی Relocate آن باید صورت گیرد. در اینجا میتوان مسیر جدید ریشه اصلی مخزن را در سیستم جدید معرفی کرد.
- تنظیمات پیش فرض باید تغییر کنند تا کلمات عبور حداقل 10 کاراکتر باشند
- کلمه عبور حداقل یک عدد و یک کاراکتر ویژه باید داشته باشد
- امکان استفاده از 5 کلمه عبور اخیری که ثبت شده وجود ندارد
ایجاد اپلیکیشن جدید
در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.
بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک میکند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.
- کنترلر Account از کلاس UserManager استفاده میکند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده میکند که برای دسترسی و مدیریت دادههای کاربران استفاده میشود. در مثال ما این کلاس از Entity Framework استفاده میکند که پیاده سازی پیش فرض است.
- متد Register POST یک کاربر جدید میسازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی میکند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); }
قانون 1: کلمههای عبور باید حداقل 10 کاراکتر باشند
- مقدار حداقل کاراکترهای کلمه عبور به دو شکل میتواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی میشود، همینجا میتوانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری میکند. سپس میتوان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext())) { PasswordValidator = new MinimumLengthValidator (10); } }
- حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
public AccountController() : this(new ApplicationUserManager()) { } public AccountController(ApplicationUserManager userManager) { UserManager = userManager; } public ApplicationUserManager UserManager { get; private set; }
- اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت میکنید.
قانون 2: کلمههای عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند
- در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده میکنیم.
public class CustomPasswordValidator : IIdentityValidator<string> { public int RequiredLength { get; set; } public CustomPasswordValidator(int length) { RequiredLength = length; } public Task<IdentityResult> ValidateAsync(string item) { if (String.IsNullOrEmpty(item) || item.Length < RequiredLength) { return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength))); } string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$"; if (!Regex.IsMatch(item, pattern)) { return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character")); } return Task.FromResult(IdentityResult.Success); }
- قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } }
قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد
public class PreviousPassword { public PreviousPassword() { CreateDate = DateTimeOffset.Now; } [Key, Column(Order = 0)] public string PasswordHash { get; set; } public DateTimeOffset CreateDate { get; set; } [Key, Column(Order = 1)] public string UserId { get; set; } public virtual ApplicationUser User { get; set; } }
- خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser { public ApplicationUser() : base() { PreviousUserPasswords = new List<PreviousPassword>(); } public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; } }
public class ApplicationUserStore : UserStore<ApplicationUser> { public ApplicationUserStore(DbContext context) : base(context) { } public override async Task CreateAsync(ApplicationUser user) { await base.CreateAsync(user); await AddToPreviousPasswordsAsync(user, user.PasswordHash); } public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password) { user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password }); return UpdateAsync(user); } }
public class ApplicationUserManager : UserManager<ApplicationUser> { private const int PASSWORD_HISTORY_LIMIT = 5; public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ResetPasswordAsync(userId, token, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } private async Task<bool> IsPreviousPassword(string userId, string newPassword) { var user = await FindByIdAsync(userId); if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate). Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT) .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any()) { return true; } return false; } }
سورس کد این مثال را میتوانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.
الف) ابتدا افزونه Razor Generator را از اینجا دریافت و نصب کنید.
ب) سپس به پروژه MVC خود بسته NuGet زیر را اضافه نمائید:
PM> Install-Package RazorGenerator.Mvc
با اضافه کردن این بسته NuGet تغییرات زیر به پروژه جاری اعمال خواهند شد:
- ارجاعی به اسمبلی RazorGenerator.Mvc.dll به پروژه اضافه خواهد شد.
- در پوشه App_Start، فایلی به نام RazorGeneratorMvcStart.cs اضافه گردیده و کار تنظیم موتور View مخصوص کار با Viewهای کامپایل شده را انجام میدهد.
ج) پس از نصب بسته NuGet یاد شده در همان خط فرمان پاورشل نوگت دستور زیر را صادر کنید:
PM> Enable-RazorGenerator
پس از انجام اینکار، دو کار صورت خواهد گرفت:
- برای تمام Viewهای برنامه، فایل cs متناظری تولید میشود که ذیل فایلهای View قابل مشاهده است.
- گزینه Custom tool این Viewها نیز به RazorGenerator تنظیم میشود.
بدیهی است اگر از دستور Enable-RazorGenerator استفاده نکنید، نیاز خواهید داشت تا تنظیم گزینه Custom tool به RazorGenerator کلیه Viewها را دستی انجام داده و اگر فایل cs متناظر با View تولید نشد روی فایل view کلیک راست کرده و گزینه run custom tool را انتخاب کنید. اما دستور Enable-RazorGenerator کار را بسیار ساده میکند.
مزایا:
- در عمل موتور ASP.NET همین کارها را در زمان اولین بار اجرای Viewها(ی کامپایل نشده) در پشت صحنه انجام میدهد. بنابراین با این روش زمان آغاز برنامه سریعتر میشود.
- دیگر نیازی به توزیع فایلهای cshtml نخواهید داشت.
- خطایابی Viewها نیز سادهتر میشود. از این جهت که کامپایل آنها به زمان اجرا موکول نخواهد شد.
یک سری قابلیتهای دیگر نیز به همراه این پروژه است مانند انتقال Viewها به یک اسمبلی دیگر و یا استفاده از MSBuild برای انجام عملیات که میتوانید آنها را در Wiki پروژه Razor Generator مطالعه کنید. انتقال Viewها به یک اسمبلی دیگر هرچند در این روش کاملا ممکن شده و کار میکند اما صفحه dialog افزودن یک view جدید مهیا در کلیک راست بر روی اکشن متدهای یک کنترلر را غیرفعال میکند که در عمل آنچنان جالب نیست.
یک نکته مهم:
اگر در آینده بسته NuGet و افزونه یاد شده را به روز کردید نیاز است دستور زیر را اجرا کنید:
PM> Redo-RazorGenerator
فایلهای Helper قرار گرفته در پوشه App_Code
اگر HTML Helperهای خود را توسط فایلهای Razor قرار گرفته در پوشه App_Code تولید میکنید، پس از اجرای دستور Enable-RazorGenerator، برای این موارد نیز فایلهای cs متناظری تولید میشود. با این تفاوت که Build Action آنها بر روی Compile قرار ندارند که این مورد را باید دستی تنظیم کنید. همچنین حین استفاده از این توابع کمکی نیاز است فضای نام مرتبط را نیز در ابتدای فایل View خود ذکر کنید مثلا:
@using MvcViewGenTest2.app_code
حذف فایل RazorGeneratorMvcStart.cs
اگر علاقمند به استفاده از فایل پیش فرض RazorGeneratorMvcStart.cs نیستید و میخواهید موتورهای View اضافی را حذف کنید، ابتدا فایل RazorGeneratorMvcStart.cs را حذف کرده و سپس در فایل global.asax.cs تغییر زیر را اعمال نمائید:
using System.Web.Http; using System.Web.Mvc; using System.Web.Routing; using System.Web.WebPages; using RazorGenerator.Mvc; namespace MvcViewGenTest2 { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); // Adding PrecompiledMvcEngine var engine = new PrecompiledMvcEngine(typeof(MvcApplication).Assembly); ViewEngines.Engines.Clear(); ViewEngines.Engines.Add(engine); VirtualPathFactoryManager.RegisterVirtualPathFactory(engine); } } }
نیاز به درایور OleDB مخصوص بانکهای اطلاعاتی قدیمی
برای کار با بانکهای اطلاعاتی قدیمی از طریق ADO.NET، نیاز است بتوان به نحوی با آنها ارتباط برقرار کرد و اینکار از طریق استاندارد OleDB که صرفا مختص به ویندوز است، قابل انجام است. برای مثال برای کار با فاکسپرو نیز در ابتدا باید درایور OleDB آنرا نصب کرد که ... هیچکدام از لینکهای قدیمی مایکروسافت در این زمینه دیگر وجود خارجی ندارند! آخرین نگارش مرتبط را میتوانید در این آدرس و ذیل نام VFPOLEDBSetup.msi دریافت کنید (نگارش 9 را نصب میکند).
نیاز به دریافت بستهی System.Data.OleDb
در اولین قدم جهت کار با درایور OleDB نصب شده، باید یک اتصال را توسط نمونه سازی شیء OleDbConnection ایجاد کرد که ... این شیء هم شناسایی نمیشود. به همین جهت باید بستهی نیوگت مرتبط با آنرا به صورت جداگانهای دریافت و نصب کرد:
<ItemGroup> <PackageReference Include="System.Data.OleDb" Version="7.0.0"/> </ItemGroup>
برنامهی مبتنی بر درایور OleDB فاکسپرو اجرا نمیشود!
اولین سعی در برقراری ارتباط با درایور OleDB نصب شده، با خطای زیر خاتمه مییابد:
The 'VFPOLEDB' provider is not registered on the local machine.
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <PlatformTarget>x86</PlatformTarget> </PropertyGroup>
روش یافتن نام پروایدر OleDB نصب شده
رشتههای اتصالی OleDB، با =Provider، شروع میشوند. اکنون این سؤال مطرح میشود که پس از نصب VFPOLEDBSetup.ms یاد شده، دقیقا چه پروایدری به سیستم اضافه شدهاست و نام آن چیست؟
برای اینکار میتوان از چندسطر زیر برای یافتن نام تمام پروایدرهای OleDB نصب شدهی بر روی سیستم استفاده کرد:
var oleDbEnumerator = new OleDbEnumerator(); using var elements = oleDbEnumerator.GetElements(); foreach (DataRow row in elements.Rows) { Console.WriteLine($"{row["SOURCES_DESCRIPTION"]}: {row["SOURCES_NAME"]}"); }
Microsoft OLE DB Provider for SQL Server: SQLOLEDB MSDataShape: MSDataShape SQL Server Native Client 11.0: SQLNCLI11 Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB OLE DB Provider for Microsoft Directory Services: ADsDSOObject Microsoft OLE DB Driver for SQL Server: MSOLEDBSQL Microsoft OLE DB Driver for SQL Server Enumerator: MSOLEDBSQL Enumerator SQL Server Native Client 11.0 Enumerator: SQLNCLI11 Enumerator Microsoft OLE DB Provider for Search: Windows Search Data Source Microsoft OLE DB Provider for ODBC Drivers: MSDASQL Microsoft OLE DB Enumerator for ODBC Drivers: MSDASQL Enumerator Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP Microsoft Jet 4.0 OLE DB Provider: Microsoft.Jet.OLEDB.4.0 Microsoft OLE DB Enumerator for SQL Server: SQLOLEDB Enumerator Microsoft OLE DB Simple Provider: MSDAOSP Microsoft OLE DB Provider for Oracle: MSDAORA
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
روش خواندن اطلاعات یک بانک اطلاعاتی فاکس پرو
پس از این مقدمات و تنظیمات، اکنون میتوانیم از قطعه کد متداول ADO.NET زیر، جهت خواندن اطلاعات یک بانک اطلاعاتی فاکسپرو، استفاده کنیم:
var connectionString = "Provider=VFPOLEDB;Data Source=" + @"C:\path\Db.DBF;Password=;Collating Sequence=MACHINE"; using var dbConnection = new OleDbConnection(connectionString); using var dataAdapter = new OleDbDataAdapter("select family from Db.DBF", dbConnection); using var dataset = new DataSet(); dataAdapter.Fill(dataset, "table1"); var sb = new StringBuilder(); foreach (DataRow dataRow in dataset.Tables[0].Rows) { var iranSystem = dataRow[0] as string; var unicode = iranSystem.FromIranSystemToUnicode(); if (!string.IsNullOrWhiteSpace(unicode)) { sb.AppendLine($"[DataRow(\"{iranSystem}\", \"{unicode}\")]"); } }
- نام پروایدر در رشته اتصالی، به VFPOLEDB تنظیم شدهاست.
- select انجام شده بر روی نام فایل dbf انجام میشود. یعنی هر فایل dbf، یک جدول را تشکیل میدهد.
- اگر نام فیلدهای موجود را نمیدانید، بجای select family از * select استفاده کنید و سپس بر روی DataSet پر شده، یک break-point را قرار دهید تا بتوانید نام تمام ستونها را از آن استخراج کنید.
- رشتهای را که توسط درایور فاکسپرو دریافت میکنید، یک رشتهی اسکی سیستم عامل داس است که در دات نت، با encoding مساوی 1252 شناخته میشود. یعنی encoding این رشتهی دریافتی، یونیکد پیشفرض داتنت نیست و باید توسط متد Encoding.GetEncoding(1252).GetBytes پردازش شود. که در نگارشهای جدید دات نت، این کدپیجها به صورت پیشفرض مهیا نیستند و باید در ابتدا ثبت شوند تا قابل استفاده شوند:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
سطح اول کش در NHibernate در یک تراکنش معنا پیدا میکند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنشهای جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.
الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریمها امکان دریافت فایلهای مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، میخواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامههای وب و دسکتاپ بدون مشکل کار میکند).
ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate میباشد. اینکار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...
ج) تعریف نوع کش در هنگام ایجاد نگاشتها
اگر از ClassMapها برای تعریف نگاشتها استفاده میکنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده میکنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;
namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
د) اعمال متد Cacheable به کوئریها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان میکنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنشهای جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();
ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینهتر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).