<button onClick={this.handleRemove} value={id}>Remove</button>
handleRemove(event) { const id = event.target.value; // ... }
و یا یک روش دیگر آن استفاده از currying است که یک متد، متد دومی را بازگشت میدهد:
handleChange(param) { //ES-5 return function (event) { }; } handleChange = param => event => {//ES-6 }; <input type="text" onChange={this.handleChange(someParam)} />
class App extends Component { oneParameter = a => e => { alert(a); }; twoParameter = (a, b) => e => { alert(a + b); }; render() { return ( <div> <button onClick={this.oneParameter(32)}> one parameter </button> <br /> <br /> <button onClick={this.twoParameter(10, 54)}> two parameter</button> </div> ); } }
کدهای کامل کامپوننت Pages\LearnBlazor\Lifecycle.razor
@page "/lifecycle" @using System.Threading <div class="border"> <h3>Lifecycles Parent Component</h3> <div class="border"> <LifecycleChild CountValue="CurrentCount"></LifecycleChild> </div> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> <br /><br /> <button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount </div> @code { int CurrentCount = 0; int MaxCount = 5; private void IncrementCount() { CurrentCount++; Console.WriteLine("Parnet - IncrementCount is called"); } protected override void OnInitialized() { Console.WriteLine("Parnet - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnInitializedAsync is called"); } protected override void OnParametersSet() { Console.WriteLine("Parnet - OnParameterSet is called"); } protected override async Task OnParametersSetAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnParametersSetAsync is called"); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called"); CurrentCount = 111; } else { CurrentCount = 999; Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine("Parnet - OnAfterRenderAsync is called"); } protected override bool ShouldRender() { Console.WriteLine("Parnet - ShouldRender is called"); return true; } void StartCountdown() { Console.WriteLine("Parnet - StartCountdown()"); var timer = new Timer(TimeCallBack, null, 1000, 1000); } void TimeCallBack(object state) { if (MaxCount > 0) { MaxCount--; Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)"); InvokeAsync(StateHasChanged); } } }
و کدهای کامل کامپوننت Pages\LearnBlazor\LearnBlazorComponents\LifecycleChild.razor
<h3 class="ml-3 mr-3">Lifecycles Child Componenet</h3> @code { [Parameter] public int CountValue { get; set; } protected override void OnInitialized() { Console.WriteLine(" Child - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine(" Child - OnInitializedAsync is called"); } protected override void OnParametersSet() { Console.WriteLine(" Child - OnParameterSet is called"); } protected override async Task OnParametersSetAsync() { await Task.Delay(100); Console.WriteLine(" Child - OnParametersSetAsync is called"); } protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine(" Child - OnAfterRender(firstRender == true) is called"); } else { Console.WriteLine(" Child - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine(" Child - OnAfterRenderAsync is called"); } protected override bool ShouldRender() { Console.WriteLine(" Child - ShouldRender is called"); return true; } }
<li class="nav-item px-3"> <NavLink class="nav-link" href="lifecycle"> <span class="oi oi-list-rich" aria-hidden="true"></span> Lifecycles </NavLink> </li>
رویدادهای OnInitialized و OnInitializedAsync
@code { protected override void OnInitialized() { Console.WriteLine("Parnet - OnInitialized is called"); } protected override async Task OnInitializedAsync() { await Task.Delay(100); Console.WriteLine("Parnet - OnInitializedAsync is called"); }
در کامپوننت Lifecycle.razor، یک کامپوننت دیگر نیز به نام LifecycleChild.razor فراخوانی شدهاست. در این حالت ابتدا OnInitialized کامپوننت والد فراخوانی شدهاست و پس از آن بلافاصله فراخوانی OnInitialized کامپوننت فرزند را مشاهده میکنیم.
رویدادهای OnParametersSet و OnParametersSetAsync
این رویدادها یکبار در زمان بارگذاری اولیهی کامپوننت و بار دیگر هر زمانیکه کامپوننت فرزند، پارامتر جدیدی را از طریق کامپوننت والد دریافت میکند، فراخوانی میشوند. برای نمونه کامپوننت LifecycleChild، پارامتر CurrentCount را از والد خود دریافت میکند:
<LifecycleChild CountValue="CurrentCount"></LifecycleChild>
Parnet - IncrementCount is called Parnet - ShouldRender is called Child - OnParameterSet is called Child - ShouldRender is called Parnet - OnAfterRender(firstRender == false) is called Child - OnAfterRender(firstRender == false) is called Child - OnParametersSetAsync is called Child - ShouldRender is called Child - OnAfterRender(firstRender == false) is called Child - OnAfterRenderAsync is called Parnet - OnAfterRenderAsync is called Child - OnAfterRenderAsync is called
رویدادهای OnAfterRender و OnAfterRenderAsync
پس از هر بار رندر کامپوننت، این متدها فراخوانی میشوند. در این مرحله کار بارگذاری کامپوننت، دریافت اطلاعات و نمایش آنها به پایان رسیدهاست. یکی از کاربردهای آن، آغاز کامپوننتهای جاوا اسکریپتی است که برای کار، نیاز به DOM را دارند؛ مانند نمایش یک modal بوت استرپی.
یک نکته: هر تغییری که در مقادیر فیلدها در این رویدادها صورت گیرند، به UI اعمال نمیشوند؛ چون در مرحلهی آخر رندر UI قرار دارند.
@code { protected override void OnAfterRender(bool firstRender) { if (firstRender) { Console.WriteLine("Parnet - OnAfterRender(firstRender == true) is called"); CurrentCount = 111; } else { CurrentCount = 999; Console.WriteLine("Parnet - OnAfterRender(firstRender == false) is called"); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await Task.Delay(100); Console.WriteLine("Parnet - OnAfterRenderAsync is called"); } }
سؤال: با توجه به مقدار دهیهای 111 و 999 صورت گرفتهی در متد OnAfterRender، در اولین بار نمایش کامپوننت، چه عددی به عنوان CurrentCount نمایش داده میشود؟
در اولین بار نمایش صفحه، لحظهای عدد 111 و سپس عدد 999 نمایش داده میشود. عدد 111 را در بار اول رندر و عدد 999 را در بار دوم رندر که پس از مقدار دهی پارامتر کامپوننت فرزند است، میتوان مشاهده کرد.
اما ... اگر پس از نمایش اولیهی صفحه، چندین بار بر روی دکمهی click me کلیک کنیم، همواره عدد 1000 مشاهده میشود. علت اینجا است که تغییرات مقادیر فیلدها در متد OnAfterRender، به UI اعمال نمیشوند؛ چون در این مرحله، رندر UI به پایان رسیدهاست. در اینجا فقط مقدار فیلد CurrentCount به 999 تغییر میکند و به همین صورت باقی میماند. دفعهی بعدی که بر روی دکمهی click me کلیک میکنیم، یک واحد به آن اضافه شده و اکنون است که کار رندر UI، مجددا شروع خواهد شد (در واکشن به یک رخداد و فراخوانی ضمنی StateHasChanged در پشت صحنه) و اینبار حاصل 999+1 را در UI مشاهده میکنیم و باز هم در پایان کار رندر، مجددا مقدار CurrentCount به 999 تغییر میکند که ... دیگر به UI منعکس نمیشود تا زمان کلیک بعدی و همینطور الی آخر.
رویدادهای StateHasChanged و ShouldRender
- اگر خروجی رویداد ShouldRender مساوی true باشد، اجازهی اعمال تغییرات به UI داده خواهد شد و برعکس. بنابراین اگر حالت UI تغییر کند و خروجی این متد false باشد، این تغییرات نمایش داده نخواهند شد.
- اگر رویداد StateHasChanged فراخوانی شود، به معنای درخواست رندر مجدد UI است. کاربرد آن در مکانهایی است که نیاز به اطلاع رسانی دستی تغییرات UI وجود دارد؛ درست پس از زمانیکه رندر UI به پایان رسیدهاست. برای آزمایش این مورد و فراخوانی دستی StateHasChanged، کدهای تایمر زیر تهیه شدهاند:
@page "/lifecycle" @using System.Threading button class="btn btn-primary" @onclick=StartCountdown>Start Countdown</button> @MaxCount @code { int MaxCount = 5; void StartCountdown() { Console.WriteLine("Parnet - StartCountdown()"); var timer = new Timer(TimeCallBack, null, 1000, 1000); } void TimeCallBack(object state) { if (MaxCount > 0) { MaxCount--; Console.WriteLine("Parnet - InvokeAsync(StateHasChanged)"); InvokeAsync(StateHasChanged); } } }
یک نکته: متدهای رویدادگردان در Blazor، میتوانند sync و یا async باشند؛ مانند متدهای OnClick و OnClickAsync زیر که هر دو پس از پایان متدها، سبب فراخوانی ضمنی StateHasChanged نیز میشوند (به این دلیل است که با کلیک بر روی دکمهای، UI هم به روز رسانی میشود). البته متدهای رویدادگردان async، دوبار سبب فراخوانی ضمنی StateHasChanged میشوند؛ یکبار زمانیکه قسمت sync متد به پایان میرسد و یکبار هم زمانیکه کار فراخوانی کلی متد به پایان خواهد رسید:
<button @onclick="OnClick">Synchronous</button> <button @onclick="OnClickAsync">Asynchronous</button> @code{ void OnClick() { } // StateHasChanged is called after the method async Task OnClickAsync() { text = "click1"; // StateHasChanged is called here as the synchronous part of the method ends await Task.Delay(1000); await Task.Delay(2000); text = "click2"; } // StateHasChanged is called after the method }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-06.zip
<script src="~/Scripts/jquery.validate.min.js" type="text/javascript"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js" type="text/javascript"></script>
عدم سازگاری پیش فرض jquery.validate با بعضی از ویجتهای Kendo UI
در حالت استفاده از Kendo UI، این سیستم هنوز هم کار میکند؛ اما با یک مشکل. اگر برای مثال از kendoComboBox استفاده کنید، اعتبارسنجیهای تعریف شده در برنامه، توسط jquery.validate دیده نخواهند شد. برای مثال فرض کنید یک چنین مدلی در اختیار View برنامه قرار گرفته است:
public class OrderDetailViewModel { [StringLength(15)] [Required] public string Destination { get; set; } }
@model Mvc4TestViewModel.Models.OrderDetailViewModel @using (Ajax.BeginForm(actionName: "Index", controllerName: "Home", ajaxOptions:new AjaxOptions(), htmlAttributes: new { id = "Form1", name ="Form1" }, routeValues: new { } )) { @Html.AntiForgeryToken() @Html.ValidationSummary(true) <fieldset> <legend>OrderDetail</legend> <div class="editor-label"> @Html.LabelFor(model => model.Destination) </div> <div class="editor-field"> @Html.TextBoxFor(model => model.Destination, new { @class = "k-textbox" }) @Html.ValidationMessageFor(model => model.Destination) </div> <p> <button class="k-button" type="submit" title="Sumbit"> Sumbit </button> </p> </fieldset> } @section JavaScript { <script type="text/javascript"> $(function () { $("#Destination").kendoComboBox({ dataSource: [ "loc 1", "loc 2" ] }); </script> }
همانطور که در تصویر مشاهده میکنید، با اتصال kendoComboBox به یک فیلد، این فیلد در حالت مخفی قرار میگیرد و ویجت کندو یو آی بجای آن نمایش داده خواهد شد. در این حالت چون در فایل jquery.validate.js چنین تنظیمی وجود دارد:
$.extend( $.validator, { defaults: { //… ignore: ":hidden",
راه حل آن نیز سادهاست. تنها باید خاصیت ignore را بازنویسی کرد و تغییر داد:
<script type="text/javascript"> $(function () { var form = $('#Form1'); form.data('validator').settings.ignore = ''; // default is ":hidden". }); </script>
<script type="text/javascript"> $.validator.setDefaults({ ignore: "" }); </script>
یکپارچه کردن سیستم اعتبارسنجی Kendo UI با سیستم اعتبارسنجی ASP.NET MVC
در مطلب «اعتبار سنجی ورودیهای کاربر در Kendo UI» با زیرساخت اعتبارسنجی Kendo UI آشنا شدید. برای اینکه بتوان این سیستم را با ASP.NET MVC یکپارچه کرد، نیاز است دو کار صورت گیرد:
الف) تعریف فایل kendo.aspnetmvc.js به صفحه اضافه شود:
<script src="~/Scripts/kendo.aspnetmvc.js" type="text/javascript"></script>
<script type="text/javascript"> $(function () { $("form").kendoValidator(); }); </script>
فایل kendo.aspnetmvc.js که در بستهی مخصوص Kendo UI تهیه شده برای ASP.NET MVC موجود است (در پوشهی js آن)، عملکردی مشابه فایل jquery.validate.unobtrusive مایکروسافت دارد. کار آن وفق دادن و ترجمهی اعتبارسنجی unobtrusive به روش Kendo UI است.
این فایل را از اینجا میتوانید دریافت کنید:
kendo.mvc.zip
البته باید دقت داشت که در حال حاضر فقط ویژگیهای ذیل از ASP.NET MVC توسط kendo.aspnetmvc.js پشتیبانی میشوند:
Required StringLength Range RegularExpression
- چطور یک اپلیکیشن وب ASP.NET MVC 5 بسازید و آن را روی یک وب سایت Windows Azure منتشر کنید.
- چگونه از OAuth، OpenID و سیستم عضویت ASP.NET برای ایمن سازی اپلیکیشن خود استفاده کنید.
- چگونه از API جدید سیستم عضویت برای مدیریت اعضا و نقشها استفاده کنید.
- چگونه از یک دیتابیس SQL برای ذخیره دادهها در Windows Azure استفاده کنید.
توجه: برای تمام کردن این مقاله به یک حساب کاربری Windows Azure نیاز دارید، که بصورت رایگان میتوانید آن را بسازید. برای اطلاعات بیشتر به Windows Azure Free Trial مراجعه کنید.
در این مقاله:
- برپایی محیط توسعه (development environment)
- برپایی محیط Windows Azure
- ایجاد یک اپلیکیشن ASP.NET MVC 5
- توزیع اپلیکیشن روی Windows Azure
- افزودن یک دیتابیس به اپلیکیشن
- افزودن یک OAuth Provider
- استفاده از Membership API
- توزیع اپلیکیشن روی Windows Azure
- قدمهای بعدی
برپایی محیط توسعه
هنگامی که این مرحله با موفقیت به اتمام رسید، تمام ابزار لازم برای شروع به کار را در اختیار دارید.
برپایی محیط Windows Azure
وب سایت Windows Azure شما در یک محیط اشتراکی (shared) میزبانی میشود، و این بدین معنا است که وب سایتهای شما روی ماشینهای مجازی (virtual machines) اجرا میشوند که با مشتریان دیگر Windows Azure به اشتراک گذاشته شده اند. یک محیط میزبانی اشتراکی گزینه ای کم هزینه برای شروع کار با رایانشهای ابری است. اگر در آینده ترافیک وب سایت شما رشد چشم گیری داشته باشد، میتوانید اپلیکیشن خود را طوری توسعه دهید که به نیازهای جدید پاسخگو باشد و آن را روی یک ماشین مجازی اختصاصی (dedicated VMs) میزبانی کنید. اگر معماری پیچیدهتری نیاز دارید، میتوانید به یک سرویس Windows Azure Cloud مهاجرت کنید. سرویسهای ابری روی ماشینهای مجازی اختصاصی اجرا میشوند که شما میتوانید تنظیمات آنها را بر اساس نیازهای خود پیکربندی کنید.
- در پرتال مدیریتی Windows Azure روی Web Sites در قسمت چپ صفحه کلیک کنید، و گزینه New را برگزینید.
- روی Web Site و سپس Custom Create کلیک کنید.
- در مرحله Create Web Site در قسمت URL یک رشته وارد کنید که آدرسی منحصر بفرد برای اپلیکیشن شما خواهد بود. آدرس کامل وب سایت شما، ترکیبی از مقدار این فیلد و مقدار روبروی آن است.
- در لیست Database گزینه Create a free 20 MB SQL Database را انتخاب کنید.
- در لیست Region همان مقداری را انتخاب کنید که برای وب سایت تان انتخاب کرده اید. تنظیمات این قسمت مشخص میکند که ماشین مجازی (VM) شما در کدام مرکز داده (data center) خواهد بود.
- در قسمت DB Connection String Name مقدار پیش فرض DefaultConnection را بپذیرید.
- دکمه فلش پایین صفحه را کلیک کنید تا به مرحله بعد، یعنی مرحله Specify Database Settings بروید.
- در قسمت Name مقدار ContactDB را وارد کنید (تصویر زیر).
- در قسمت Server گزینه New SQL Database Server را انتخاب کنید. اگر قبلا دیتابیس ساخته اید میتوانید آن را از کنترل dropdown انتخاب کنید.
- مقدار قسمت Region را به همان مقداری که برای ایجاد وب سایت تان تنظیم کرده اید تغییر دهید.
- یک Login Name و Password مدیر (administrator) وارد کنید. اگر گزینه New SQL Database server را انتخاب کرده اید، چنین کاربری وجود ندارد و در واقع اطلاعات یک حساب کاربری جدید را وارد میکنید تا بعدا هنگام دسترسی به دیتابیس از آن استفاده کنید. اگر دیتابیس دیگری را از لیست انتخاب کرده باشید، اطلاعات یک حساب کاربری موجود از شما دریافت خواهد شد. در مثال این مقاله ما گزینه Advanced را رها میکنیم. همچنین در نظر داشته باشید که برای دیتابیسهای رایگان تنها از یک Collation میتوانید استفاده کنید.
دکمه تایید پایین صفحه را کلیک کنید تا مراحل تمام شود.
تصویر زیر استفاده از یک SQL Server و حساب کاربری موجود (existing) را نشان میدهد.
پرتال مدیریتی پس از اتمام مراحل، به صفحه وب سایتها باز میگردد. ستون Status نشان میدهد که سایت شما در حال ساخته شدن است. پس از مدتی (معمولا کمتر از یک دقیقه) این ستون نشان میدهد که سایت شما با موفقیت ایجاد شده. در منوی پیمایش سمت چپ، تعداد سایت هایی که در اکانت خود دارید در کنار آیکون Web Sites نمایش داده شده است، تعداد دیتابیسها نیز در کنار آیکون SQL Databases نمایش داده میشود.
یک اپلیکیشن ASP.NET MVC 5 بسازید
نوع پروژه را ASP.NET Web Application انتخاب کنید.
نکته: در تصویر بالا نام پروژه "MyExample" است اما حتما نام پروژه خود را به "ContactManager" تغییر دهید. قطعه کدهایی که در ادامه مقاله خواهید دید نام پروژه را ContactManager فرض میکنند.
در دیالوگ جدید ASP.NET نوع اپلیکیشن را MVC انتخاب کنید و دکمه Change Authentication را کلیک کنید.
گزینه پیش فرض Individual User Accounts را بپذیرید. برای اطلاعات بیشتر درباره متدهای دیگر احراز هویت به این لینک مراجعه کنید. دکمههای OK را کلیک کنید تا تمام مراحل تمام شوند.
تنظیم تیتر و پاورقی سایت
- فایل Layout.cshtml_ را باز کنید. دو نمونه از متن "My ASP.NET MVC Application" را با عبارت "Contact Manager" جایگزین کنید.
- عبارت "Application name" را هم با "CM Demo" جایگزین کنید.
اپلیکیشن را بصورت محلی اجرا کنید
اپلیکیشن شما فعلا آماده است و میتوانید آن را روی Windows Azure توزیع کنید. بعدا دیتابیس و دسترسی داده نیز اضافه خواهد شد.
اپلیکیشن را روی Windows Azure منتشر کنید
حال دیالوگ Import Publish Profile نمایش داده میشود.
یکی از متدهای زیر را استفاده کنید تا ویژوال استودیو بتواند به اکانت Windows Azure شما متصل شود.
- روی Sign In کلیک کنید تا با وارد کردن اطلاعات حساب کاربری وارد Windows Azure شوید.
- روی Manage subscriptions کلیک کنید تا یک management certificate نصب کنید، که دسترسی به حساب کاربری شما را ممکن میسازد.
در دیالوگ باکس Publish Web روی Publish کلیک کنید.
اپلیکیشن شما حالا در فضای ابری اجرا میشود. دفعه بعد که اپلیکیشن را منتشر کنید تنها فایلهای تغییر کرده (یا جدید) آپلود خواهند شد.
یک دیتابیس به اپلیکیشن اضافه کنید
کلاسهای مدل Contacts را اضافه کنید
نام کلاس را به Contact.cs تغییر دهید و دکمه Add را کلیک کنید.
کد فایل Contact.cs را با قطعه کد زیر مطابقت دهید.
using System.ComponentModel.DataAnnotations; using System.Globalization; namespace ContactManager.Models { public class Contact { public int ContactId { get; set; } public string Name { get; set; } public string Address { get; set; } public string City { get; set; } public string State { get; set; } public string Zip { get; set; } [DataType(DataType.EmailAddress)] public string Email { get; set; } } }
این کلاس موجودیت Contact را در دیتابیس معرفی میکند. داده هایی که میخواهیم برای هر رکورد ذخیره کنیم تعریف شده اند، بعلاوه یک فیلد Primary Key که دیتابیس به آن نیاز دارد.
یک کنترلر و نما برای دادهها اضافه کنید
در دیالوگ باکس Add Scaffold گزینه MVC 5 Controller with views, using EF را انتخاب کنید.
در دیالوگ Add Controller نام "CmController" را برای کنترلر وارد کنید. (تصویر زیر.)
در لیست Model گزینه (Contact (ContactManager.Models را انتخاب کنید.
در قسمت Data context class گزینه (ApplicationDbContext (ContactManager.Models را انتخاب کنید. این ApplicationDbContext هم برای اطلاعات سیستم عضویت و هم برای دادههای Contacts استفاده خواهد شد.
روی Add کلیک کنید. ویژوال استودیو بصورت خودکار با استفاده از Scaffolding متدها و Viewهای لازم برای عملیات CRUD را فراهم میکند، که همگی از مدل Contact استفاده میکنند.
فعالسازی مهاجرت ها، ایجاد دیتابیس، افزودن داده نمونه و یک راه انداز
در پنجره باز شده فرمان زیر را وارد کنید.
enable-migrations
فرمان enable-migrations یک پوشه با نام Migrations می سازد و فایلی با نام Configuration.cs را به آن اضافه میکند. با استفاده از این کلاس میتوانید دادههای اولیه دیتابیس را وارد کنید و مهاجرتها را نیز پیکربندی کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
add-migration Initial
فرمان add-migration initial فایلی با نام data_stamp> initial> ساخته و آن را در پوشه Migrations ذخیره میکند. در این مرحله دیتابیس شما ایجاد میشود. در این فرمان، مقدار initial اختیاری است و صرفا برای نامگذاری فایل مهاجرت استفاده شده. فایلهای جدید را میتوانید در Solution Explorer مشاهده کنید.
در کلاس Initial متد Up جدول Contacts را میسازد. و متد Down (هنگامی که میخواهید به وضعیت قبلی بازگردید) آن را drop میکند.
حال فایل Migrations/Configuration.cs را باز کنید. فضای نام زیر را اضافه کنید.
using ContactManager.Models;
حال متد Seed را با قطعه کد زیر جایگزین کنید.
protected override void Seed(ContactManager.Models.ApplicationDbContext context) { context.Contacts.AddOrUpdate(p => p.Name, new Contact { Name = "Debra Garcia", Address = "1234 Main St", City = "Redmond", State = "WA", Zip = "10999", Email = "debra@example.com", }, new Contact { Name = "Thorsten Weinrich", Address = "5678 1st Ave W", City = "Redmond", State = "WA", Zip = "10999", Email = "thorsten@example.com", }, new Contact { Name = "Yuhong Li", Address = "9012 State st", City = "Redmond", State = "WA", Zip = "10999", Email = "yuhong@example.com", }, new Contact { Name = "Jon Orton", Address = "3456 Maple St", City = "Redmond", State = "WA", Zip = "10999", Email = "jon@example.com", }, new Contact { Name = "Diliana Alexieva-Bosseva", Address = "7890 2nd Ave E", City = "Redmond", State = "WA", Zip = "10999", Email = "diliana@example.com", } ); }
این متد دیتابیس را Seed میکند، یعنی دادههای پیش فرض و اولیه دیتابیس را تعریف میکند. برای اطلاعات بیشتر به Seeding and Debugging Entity Framework (EF) DBs مراجعه کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
update-database
فرمان update-database مهاجرت نخست را اجرا میکند، که دیتابیس را میسازد. بصورت پیش فرض این یک دیتابیس SQL Server Express LocalDB است.
حال پروژه را با CTRL + F5 اجرا کنید.
همانطور که مشاهده میکنید، اپلیکیشن دادههای اولیه (Seed) را نمایش میدهد، و لینک هایی هم برای ویرایش، حذف و مشاهده جزئیات رکوردها فراهم میکند. میتوانید دادهها را مشاهده کنید، رکورد جدید ثبت کنید و یا دادههای قبلی را ویرایش و حذف کنید.
یک تامین کننده OAuth2 و OpenID اضافه کنید
استفاده از Membership API
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework;
bool AddUserAndRole(ContactManager.Models.ApplicationDbContext context) { IdentityResult ir; var rm = new RoleManager<IdentityRole> (new RoleStore<IdentityRole>(context)); ir = rm.Create(new IdentityRole("canEdit")); var um = new UserManager<ApplicationUser>( new UserStore<ApplicationUser>(context)); var user = new ApplicationUser() { UserName = "user1", }; ir = um.Create(user, "Passw0rd1"); if (ir.Succeeded == false) return ir.Succeeded; ir = um.AddToRole(user.Id, "canEdit"); return ir.Succeeded; }
protected override void Seed(ContactManager.Models.ApplicationDbContext context) { AddUserAndRole(context); context.Contacts.AddOrUpdate(p => p.Name, // Code removed for brevity }
کدی موقتی برای تخصیص نقش canEdit به کاربران جدید Social Provider ها
await UserManager.AddToRoleAsync(user.Id, "CanEdit");
در ادامه مقاله اپلیکیشن خود را روی Windows Azure منتشر خواهید کرد و با استفاده از Google و تامین کنندگان دیگر وارد سایت میشوید. هر فردی که به آدرس سایت شما دسترسی داشته باشد، و یک حساب کاربری Google هم در اختیار داشته باشد میتواند در سایت شما ثبت نام کند و سپس دیتابیس را ویرایش کند. برای جلوگیری از دسترسی دیگران، میتوانید وب سایت خود را متوقف (stop) کنید.
در پنجره Package Manager Console فرمان زیر را وارد کنید.
Update-Database
فرمان را اجرا کنید تا متد Seed را فراخوانی کند. حال AddUserAndRole شما نیز اجرا میشود. تا این مرحله نقش canEdit ساخته شده و کاربر جدیدی با نام user1 ایجاد و به آن افزوده شده است.
محافظت از اپلیکیشن توسط SSL و خاصیت Authorize
در این قسمت شما با استفاده از خاصیت Authorize دسترسی به اکشن متدها را محدود میکنید. کاربران ناشناس (Anonymous) تنها قادر به مشاهده متد Index در کنترلر home خواهند بود. کاربرانی که ثبت نام کرده اند به متدهای Index و Details در کنترلر Cm و صفحات About و Contact نیز دسترسی خواهند داشت. همچنین دسترسی به متدهایی که دادهها را تغییر میدهند تنها برای کاربرانی وجود دارد که در نقش canEdit هستند.
خاصیت Authorize و RequireHttps را به اپلیکیشن اضافه کنید. یک راه دیگر افزودن این خاصیتها به تمام کنترلرها است، اما تجارب امنیتی توصیه میکند که این خاصیتها روی کل اپلیکیشن اعمال شوند. با افزودن این خاصیتها بصورت global تمام کنترلرها و اکشن متدهایی که میسازید بصورت خودکار محافظت خواهند شد، و دیگر لازم نیست بیاد داشته باشید کدام کنترلرها و متدها را باید ایمن کنید.
برای اطلاعات بیشتر به Securing your ASP.NET MVC App and the new AllowAnonymous Attribute مراجعه کنید.
فایل App_Start/FilterConfig.cs را باز کنید و متد RegisterGlobalFilters را با کد زیر مطابقت دهید.
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new System.Web.Mvc.AuthorizeAttribute()); filters.Add(new RequireHttpsAttribute()); }
خاصیت Authorize در کد بالا از دسترسی کاربران ناشناس به تمام متدهای اپلیکیشن جلوگیری میکند. شما برای اعطای دسترسی به متدهایی خاص از خاصیت AllowAnonymous استفاده خواهید کرد. در آخر خاصیت RequireHTTPS باعث میشود تا تمام دسترسیها به اپلیکیشن وب شما از طریق HTTPS صورت گیرد.
حالا خاصیت AllowAnonymous را به متد Index در کنترلر Home اضافه کنید. از این خاصیت برای اعطای دسترسی به تمامی کاربران سایت استفاده کنید. قسمتی از کد کنترلر Home را در زیر میبینید.
namespace ContactManager.Controllers { public class HomeController : Controller { [AllowAnonymous] public ActionResult Index() { return View(); }
یک جستجوی عمومی برای عبارت AllowAnonymous انجام دهید. همانطور که مشاهده میکنید این خاصیت توسط متدهای ورود و ثبت نام در کنترلر Account نیز استفاده شده است.
در کنترلر CmController خاصیت [("Authorize(Roles="canEdit] را به تمام متدهایی که با داده سر و کار دارند اضافه کنید، به غیر از متدهای Index و Details. قسمتی از کد کامل شده در زیر آمده است.
فعال سازی SSL برای پروژه
روی نام پروژه کلیک راست کنید و Properties را انتخاب کنید. در قسمت چپ گزینه Web را انتخاب کنید. حالا مقدار Project Url را به آدرسی که کپی کرده اید تغییر دهید. نهایتا تغییرات را ذخیره کنید و پنجره را ببندید.
حال پروژه را اجرا کنید. مرورگر شما باید یک پیام خطای اعتبارسنجی به شما بدهد. دلیلش این است که اپلیکیشن شما از یک Valid Certificate استفاده نمیکند. هنگامی که پروژه را روی Windows Azure منتشر کنید دیگر این پیغام را نخواهید دید. چرا که سرورهای مایکروسافت همگی لایسنسهای معتبری دارند. برای اپلیکیشن ما میتوانید روی Continue to this website را انتخاب کنید.
حال مرورگر پیش فرض شما باید صفحه Index از کنترلر home را به شما نمایش دهد.
اگر از یک نشست قبلی هنوز در سایت هستید (logged-in) روی لینک Log out کلیک کنید و از سایت خارج شوید.
روی لینکهای About و Contact کلیک کنید. باید به صفحه ورود به سایت هدایت شوید چرا که کاربران ناشناس اجازه دسترسی به این صفحات را ندارند.
روی لینک Register کلیک کنید و یک کاربر محلی با نام Joe بسازید. حال مطمئن شوید که این کاربر به صفحات Home, About و Contact دسترسی دارد.
روی لینک CM Demo کلیک کنید و مطمئن شوید که دادهها را مشاهده میکنید.
حال روی یکی از لینکهای ویرایش (Edit) کلیک کنید. این درخواست باید شما را به صفحه ورود به سایت هدایت کند، چرا که کاربران محلی جدید به نقش canEdit تعلق ندارند.
با کاربر user1 که قبلا ساختید وارد سایت شوید. حال به صفحه ویرایشی که قبلا درخواست کرده بودید هدایت میشوید.
اگر نتوانستید با این کاربر به سایت وارد شوید، کلمه عبور را از سورس کد کپی کنید و مجددا امتحان کنید. اگر همچنان نتوانستید به سایت وارد شوید، جدول AspNetUsers را بررسی کنید تا مطمئن شوید کاربر user1 ساخته شده است. این مراحل را در ادامه مقاله خواهید دید.
در آخر اطمینان حاصل کنید که میتوانید دادهها را تغییر دهید.
اپلیکیشن را روی Windows Azure منتشر کنید
در دیالوگ باز شده روی قسمت Settings کلیک کنید. روی File Publish Options کلیک کنید تا بتوانید Remote connection string را برای ApplicationDbContext و دیتابیس ContactDB انتخاب کنید.
اگر ویژوال استودیو را پس از ساخت Publish profile بسته و دوباره باز کرده اید، ممکن است رشته اتصال را در لیست موجود نبینید. در چنین صورتی، بجای ویرایش پروفایل انتشار، یک پروفایل جدید بسازید. درست مانند مراحلی که پیشتر دنبال کردید.
زیر قسمت ContactManagerContext گزینه Execute Code First Migrations را انتخاب کنید.
حال Publish را کلیک کنید تا اپلیکیشن شما منتشر شود. با کاربر user1 وارد سایت شوید و بررسی کنید که میتوانید دادهها را ویرایش کنید یا خیر.
حال از سایت خارج شوید و توسط یک اکانت Google یا Facebook وارد سایت شوید، که در این صورت نقش canEdit نیز به شما تعلق میگیرد.
برای جلوگیری از دسترسی دیگران، وب سایت را متوقف کنید
یک راه دیگر متوقف کردن وب سایت از طریق پرتال مدیریت Windows Azure است.
فراخوانی AddToRoleAsync را حذف و اپلیکیشن را منتشر و تست کنید
await UserManager.AddToRoleAsync(user.Id, "CanEdit");
دکمه Start Preview را فشار دهید. در این مرحله تنها فایل هایی که نیاز به بروز رسانی دارند آپلود خواهند شد.
وب سایت را راه اندازی کنید. سادهترین راه از طریق پرتال مدیریت Windows Azure است. توجه داشته باشید که تا هنگامی که وب سایت شما متوقف شده، نمیتوانید اپلیکیشن خود را منتشر کنید.
حال به ویژوال استودیو بازگردید و اپلیکیشن را منتشر کنید. اپلیکیشن Windows Azure شما باید در مرورگر پیش فرض تان باز شود. حال شما در حال مشاهده صفحه اصلی سایت بعنوان یک کاربر ناشناس هستید.
روی لینک About کلیک کنید، که شما را به صفحه ورود هدایت میکند.
روی لینک Register در صفحه ورود کلیک کنید و یک حساب کاربری محلی بسازید. از این حساب کاربری برای این استفاده میکنیم که ببینیم شما به صفحات فقط خواندنی (read-only) و نه صفحاتی که دادهها را تغییر میدهند دسترسی دارید یا خیر. بعدا در ادامه مقاله، دسترسی حسابهای کاربری محلی (local) را حذف میکنیم.
مطمئن شوید که به صفحات About و Contact دسترسی دارید.
لینک CM Demo را کلیک کنید تا به کنترلر CmController هدایت شوید.
روی یکی از لینکهای Edit کلیک کنید. این کار شما را به صفحه ورود به سایت هدایت میکند. در زیر قسمت User another service to log in یکی از گزینههای Google یا Facebook را انتخاب کنید و توسط حساب کاربری ای که قبلا ساختید وارد شوید.
حال بررسی کنید که امکان ویرایش اطلاعات را دارید یا خیر.
نکته: شما نمیتوانید در این اپلیکیشن از اکانت گوگل خود خارج شده، و با همان مرورگر با اکانت گوگل دیگری وارد اپلیکیشن شوید. اگر دارید از یک مرورگر استفاده میکنید، باید به سایت گوگل رفته و از آنجا خارج شوید. برای وارد شدن به اپلیکیشن توسط یک اکانت دیگر میتوانید از یک مرورگر دیگر استفاده کنید.
دیتابیس SQL Azure را بررسی کنید
توجه: اگر نمیتوانید گره SQL Databases را باز کنید و یا ContactDB را در ویژوال استودیو نمیبینید، باید مراحلی را طی کنید تا یک پورت یا یکسری پورت را به فایروال خود اضافه کنید. دقت داشته باشید که در صورت اضافه کردن Port Rangeها ممکن است چند دقیقه زمان نیاز باشد تا بتوانید به دیتابیس دسترسی پیدا کنید.
روی جدول AspNetUsers کلیک راست کرده و View Data را انتخاب کنید.
حالا روی AspNetUserRoles کلیک راست کنید و View Data را انتخاب کنید.
اگر شناسه کاربران (User ID) را بررسی کنید، مشاهده میکنید که تنها دو کاربر user1 و اکانت گوگل شما به نقش canEdit تعلق دارند.
Cannot open server login error
شما باید آدرس IP خود را به لیست آدرسهای مجاز (Allowed IPs) اضافه کنید. در پرتال مدیریتی Windows Azure در قسمت چپ صفحه، گزینه SQL Databases را انتخاب کنید.
دیتابیس مورد نظر را انتخاب کنید. حالا روی لینک Set up Windows Azure firewall rules for this IP address کلیک کنید.
هنگامی که با پیغام "?The current IP address xxx.xxx.xxx.xxx is not included in existing firewall rules. Do you want to update the firewall rules" مواجه شدید Yes را کلیک کنید. افزودن یک آدرس IP بدین روش معمولا کافی نیست و در فایروالهای سازمانی و بزرگ باید Range بیشتری را تعریف کنید.
مرحله بعد اضافه کردن محدوده آدرسهای مجاز است.
مجددا در پرتال مدیریتی Windows Azure روی SQL Databases کلیک کنید. سروری که دیتابیس شما را میزبانی میکند انتخاب کنید.
در بالای صفحه لینک Configure را کلیک کنید. حالا نام rule جدید، آدرس شروع و پایان را وارد کنید.
در پایین صفحه Save را کلیک کنید.
در آخر میتوانید توسط SSOX به دیتابیس خود متصل شوید. از منوی View گزینه SQL Server Object Explorer را انتخاب کنید. روی SQL Server کلیک راست کرده و Add SQL Server را انتخاب کنید.
در دیالوگ Connect to Server متد احراز هویت را به SQL Server Authentication تغییر دهید. این کار نام سرور و اطلاعات ورود پرتال Windows Azure را به شما میدهد.
در مرورگر خود به پرتال مدیریتی بروید و SQL Databases را انتخاب کنید. دیتابیس ContactDB را انتخاب کرده و روی View SQL Database connection strings کلیک کنید. در صفحه Connection Strings مقادیر Server و User ID را کپی کنید. حالا مقادیر را در دیالوگ مذکور در ویژوال استودیو بچسبانید. مقدار فیلد User ID در قسمت Login وارد میشود. در آخر هم کلمه عبوری که هنگام ساختن دیتابیس تنظیم کردید را وارد کنید.
حالا میتوانید با مراحلی که پیشتر توضیح داده شد به دیتابیس Contact DB مراجعه کنید.
افزودن کاربران به نقش canEdit با ویرایش جداول دیتابیس
حالا RoleId را کپی کنید و در ردیف جدید بچسبانید.
شناسه کاربر مورد نظر را از جدول AspNetUsers پیدا کنید و مقدار آن را در ردیف جدید کپی کنید. همین! کاربر جدید شما به نقش canEdit اضافه شد.
نکاتی درباره ثبت نام محلی (Local Registration)
- در کنترلر Account متدهای Register را ویرایش کنید و خاصیت AllowAnonymous را از آنها حذف کنید (هر دو متد GET و POST). این کار ثبت نام کاربران ناشناس و بدافزارها (bots) را غیر ممکن میکند.
- در پوشه Views/Shared فایل LoginPartial.cshtml_ را باز کنید و لینک Register را از آن حذف کنید.
- در فایل Views/Account/Login.cshtml نیز لینک Register را حذف کنید.
- اپلیکیشن را دوباره منتشر کنید.
قدمهای بعدی
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key
T attachedEntity = set.Find(entity.Id); var attachedEntry = dbContext.Entry(attachedEntity); attachedEntry.CurrentValues.SetValues(entity);
public DbContext() { this.Configuration.ProxyCreationEnabled = false; this.Configuration.LazyLoadingEnabled = false; this.Configuration.AutoDetectChangesEnabled = false; }
dbContext.Entry(entity).State = EntityState.Unchanged ; dbContext.Entry(entity).State = EntityState.Added ; //or Dbset.Add(entity) dbContext.Entry(entity).State = EntityState.Modified ; dbContext.Entry(entity).State = EntityState.Deleted ; // or Dbset.Remove(entity)
dbContext.Entry(entity).State = EntityState.Detached;
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);// groupstate=EntityState.Added
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(group).State = EntityState.Unchanged; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);// groupstate=EntityState.Unchanged
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(customer).State = EntityState.Unchanged; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);//// groupstate=EntityState.Unchanged
var customer = dbContext.Customers.FirstOrDefault(); var customerAsNoTracking = dbContext.Customers.AsNoTracking().FirstOrDefault(); var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Unchanged var customerstateAsNoTracking = dbContext.Entry(customerAsNoTracking).State;// customerstate=EntityState.Detached
var Entries = dbContext.ChangeTracker.Entries(); var AddedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Added); var ModifiedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Modified); var UnchangedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Unchanged); var DeletedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Deleted); var DetachedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Detached);//* not working !
var CustomersWithGroup = dbContext.Customers.AsNoTracking().Include("Group").ToList(); var CustomerFull = dbContext.Customers.AsNoTracking().Include("Group").Include("Bills").Include("Bills.BillDetails").ToList();
public virtual void AddOrUpdate(T entity) { if (entity.Id == 0) Add(entity); else Update(entity); }
5) اگر بخواهیم موجودیتهای رابطه ای در دیتا گرید ویو (ویندوز فرم) نشون بدیم باید چه کار کنیم؟
گرید ویو در ویندوز فرم قادر به نشون دادن فیلدهای رابطه ای نیست برای حل این مشکل میتونیم یک نوع ستون جدید برای گرید ویو تعریف کنیم و برای نشون دادن فیلدهای رابطه ای از این نوع ستون استفاده کنیم:
public class DataGridViewChildRelationTextBoxCell : DataGridViewTextBoxCell { protected override object GetValue(int rowIndex) { try { var bs = (BindingSource)DataGridView.DataSource; var cl = (DataGridViewChildRelationTextBoxColumn)DataGridView.Columns[ColumnIndex]; return getChildValue(bs.List[rowIndex], cl.DataPropertyName).ToString(); } catch (Exception) { return ""; } } private object getChildValue(object dataSource, string childMember) { int nextPoint = childMember.IndexOf('.'); if (nextPoint == -1) return dataSource.GetType().GetProperty(childMember).GetValue(dataSource, null); string proName = childMember.Substring(0, nextPoint); object newDs = dataSource.GetType().GetProperty(proName).GetValue(dataSource, null); return getChildValue(newDs, childMember.Substring(nextPoint + 1)); } } public class DataGridViewChildRelationTextBoxColumn : DataGridViewTextBoxColumn { public string DataMember { get; set; } public DataGridViewChildRelationTextBoxColumn() { CellTemplate = new DataGridViewChildRelationTextBoxCell(); } }
نحوه استفاده را در ادامه میبینید. این روش توسط ویزارد گریدویو هم قابل استفاده است. موقع Add کردن Column نوع اون رو روی DataGridViewChildRelationTextBoxColumn تنظیم کنید.
GroupNameColumn= new DataGridViewChildRelationTextBoxColumn(); //from your class GroupNameColumn.HeaderText = "گروه مشتری"; GroupNameColumn.DataPropertyName = "Group.Name"; //EF Property: Customer.Group.Name GroupNameColumn.Visible = true; GroupNameColumn.Width = 300; DataGridView.Columns.Add(GroupNameColumn);
مدل برنامه
در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
namespace KendoUI03.Models { public class Product { public int Id { set; get; } public string Name { set; get; } public decimal Price { set; get; } public bool IsAvailable { set; get; } } }
پیشنیاز تامین داده مخصوص Kendo UI Grid
برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواستهای صفحه بندی خود را با فرمت ذیل ارسال میکند. همانطور که مشاهده میکنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
/api/products?{"take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
{ "Data": [ {"Id":1500,"Name":"نام 1500","Price":2499.0,"IsAvailable":false}, {"Id":1499,"Name":"نام 1499","Price":2498.0,"IsAvailable":true} ], "Total":1500, "Aggregates":null }
میتوان برای تمام اینها، کلاس و Parser تهیه کرد و یا ... پروژهی سورس بازی به نام Kendo.DynamicLinq نیز چنین کاری را میسر میسازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
PM> Install-Package Kendo.DynamicLinq
تامین کنندهی داده سمت سرور
همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابیهای مخصوص آنرا به فایل global.asax.cs نیز اضافه نمائید.
using System.Linq; using System.Net.Http; using System.Web.Http; using Kendo.DynamicLinq; using KendoUI03.Models; using Newtonsoft.Json; namespace KendoUI03.Controllers { public class ProductsController : ApiController { public DataSourceResult Get(HttpRequestMessage requestMessage) { var request = JsonConvert.DeserializeObject<DataSourceRequest>( requestMessage.RequestUri.ParseQueryString().GetKey(0) ); var list = ProductDataSource.LatestProducts; return list.AsQueryable() .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter); } } }
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده میشود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شدهی Data، Total و Aggregates را تولید میکند.
تا اینجا کارهای سمت سرور این مثال به پایان میرسد.
تهیه View نمایش اطلاعات ارسالی از سمت سرور
اعمال مباحث بومی سازی
<head> <meta charset="utf-8" /> <meta http-equiv="Content-Language" content="fa" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>Kendo UI: Implemeting the Grid</title> <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" /> <!--شیوه نامهی مخصوص راست به چپ سازی--> <link href="styles/kendo.rtl.min.css" rel="stylesheet" /> <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" /> <script src="js/jquery.min.js" type="text/javascript"></script> <script src="js/kendo.all.min.js" type="text/javascript"></script> <!--محل سفارشی سازی پیامها و مسایل بومی--> <script src="js/cultures/kendo.culture.fa-IR.js" type="text/javascript"></script> <script src="js/cultures/kendo.culture.fa.js" type="text/javascript"></script> <script src="js/messages/kendo.messages.en-US.js" type="text/javascript"></script> <style type="text/css"> body { font-family: tahoma; font-size: 9pt; } </style> <script type="text/javascript"> // جهت استفاده از فایل: kendo.culture.fa-IR.js kendo.culture("fa-IR"); </script> </head>
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شدهاند. فایلهای fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شدهاند، اما نام ماههای موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایلها، جهت ویرایش نهایی نیاز خواهد بود که در پوشهی src\js\cultures مجموعهی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیامهای مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا میتوانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شدهاست. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آنرا به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.
راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
<div class="k-rtl"> <div id="report-grid"></div> </div>
تامین داده و نمایش گرید
در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه میکنید:
<script type="text/javascript"> $(function () { var productsDataSource = new kendo.data.DataSource({ transport: { read: { url: "api/products", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET' }, parameterMap: function (options) { return kendo.stringify(options); } }, schema: { data: "Data", total: "Total", model: { fields: { "Id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است "Name": { type: "string" }, "IsAvailable": { type: "boolean" }, "Price": { type: "number" } } } }, error: function (e) { alert(e.errorThrown); }, pageSize: 10, sort: { field: "Id", dir: "desc" }, serverPaging: true, serverFiltering: true, serverSorting: true }); $("#report-grid").kendoGrid({ dataSource: productsDataSource, autoBind: true, scrollable: false, pageable: true, sortable: true, filterable: true, reorderable: true, columnMenu: true, columns: [ { field: "Id", title: "شماره", width: "130px" }, { field: "Name", title: "نام محصول" }, { field: "IsAvailable", title: "موجود است", template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>' }, { field: "Price", title: "قیمت", format: "{0:c}" } ] }); }); </script>
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم میگیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیفها در کدام فیلد قرار گرفتهاست.
- نحوهی تعریف model را نیز در اینجا ملاحظه میکنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمیتوان عدد 1 رشتهای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شدهاند. اگر این مقدار دهیها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.
پس از تعریف DataSource، تنها کافی است آنرا به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب میشود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام میکنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال میکند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستونها با کلیک بر روی سرستونها فعال میگردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانهی Kendo.DynamicLinq حاصل آنرا در سمت سرور مدیریت میکند.
- reorderable: true سبب میشود تا کاربر بتواند محل قرارگیری ستونها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستونها نیز مسیر خواهد شد.
- در آخر ستونهای گرید مشخص شدهاند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت میگردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
KendoUI03.zip
برای بهبود این وضعیت میتوان مرحلهی دومی را نیز به این فرآیند لاگین افزود؛ پس از اینکه مشخص شد کاربر وارد شدهی به سایت، دارای اکانتی در IDP ما است، کدی را به آدرس ایمیل او ارسال میکنیم. اگر این ایمیل واقعا متعلق به این شخص است، بنابراین قادر به دسترسی به آن، خواندن و ورود آن به برنامهی ما نیز میباشد. این اعتبارسنجی دو مرحلهای را میتوان به عملیات لاگین متداول از طریق ورود نام کاربری و کلمهی عبور در IDP ما نیز اضافه کرد.
تنظیم میانافزار Cookie Authentication
مرحلهی اول ایجاد گردش کاری اعتبارسنجی دو مرحلهای، فعالسازی میانافزار Cookie Authentication در برنامهی IDP است. برای این منظور به کلاس Startup آن مراجعه کرده و AddCookie را اضافه میکنیم:
namespace DNT.IDP { public class Startup { public const string TwoFactorAuthenticationScheme = "idsrv.2FA"; public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddCookie(authenticationScheme: TwoFactorAuthenticationScheme) .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
اصلاح اکشن متد Login برای هدایت کاربر به صفحهی ورود اطلاعات کد موقتی
تا این مرحله، در اکشن متد Login کنترلر Account، اگر کاربر، اطلاعات هویتی خود را صحیح وارد کند، به سیستم وارد خواهد شد. برای لغو این عملکرد پیشفرض، کدهای HttpContext.SignInAsync آنرا حذف کرده و با Redirect به اکشن متد نمایش صفحهی ورود کد موقتی ارسال شدهی به آدرس ایمیل کاربر، جایگزین میکنیم.
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class AccountController : Controller { [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // ... if (ModelState.IsValid) { if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password)) { var user = await _usersService.GetUserByUsernameAsync(model.Username); var id = new ClaimsIdentity(); id.AddClaim(new Claim(JwtClaimTypes.Subject, user.SubjectId)); await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id)); await _twoFactorAuthenticationService.SendTemporaryCodeAsync(user.SubjectId); var redirectToAdditionalFactorUrl = Url.Action("AdditionalAuthenticationFactor", new { returnUrl = model.ReturnUrl, rememberLogin = model.RememberLogin }); // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(redirectToAdditionalFactorUrl); } if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } // something went wrong, show form with error var vm = await BuildLoginViewModelAsync(model); return View(vm); }
- سپس بر اساس Id این کاربر، یک ClaimsIdentity تشکیل میشود.
- در ادامه با فراخوانی متد SignInAsync بر روی این ClaimsIdentity، یک کوکی رمزنگاری شده را با scheme تعیین شده که با authenticationScheme تنظیم شدهی در کلاس آغازین برنامه تطابق دارد، ایجاد میکنیم.
await HttpContext.SignInAsync(scheme: Startup.TwoFactorAuthenticationScheme, principal: new ClaimsPrincipal(id));
public interface ITwoFactorAuthenticationService { Task SendTemporaryCodeAsync(string subjectId); Task<bool> IsValidTemporaryCodeAsync(string subjectId, string code); }
- متد IsValidTemporaryCodeAsync، کد دریافت شدهی از کاربر را با نمونهی موجود در بانک اطلاعاتی مقایسه و اعتبار آنرا اعلام میکند.
ایجاد اکشن متد AdditionalAuthenticationFactor و View مرتبط با آن
پس از ارسال کد موقتی به کاربر، کاربر را به صورت خودکار به اکشن متد جدید AdditionalAuthenticationFactor هدایت میکنیم تا این کد موقتی را که به صورت ایمیل (و یا در اینجا با مشاهدهی لاگ برنامه)، دریافت کردهاست، وارد کند. همچنین returnUrl را نیز به این اکشن متد جدید ارسال میکنیم تا بدانیم پس از ورود موفق کد موقتی توسط کاربر، او را باید در ادامهی این گردش کاری به کجا هدایت کنیم. بنابراین قسمت بعدی کار، ایجاد این اکشن متد و تکمیل View آن است.
ViewModel ای که بیانگر ساختار View مرتبط است، چنین تعریفی را دارد:
using System.ComponentModel.DataAnnotations; namespace DNT.IDP.Controllers.Account { public class AdditionalAuthenticationFactorViewModel { [Required] public string Code { get; set; } public string ReturnUrl { get; set; } public bool RememberLogin { get; set; } } }
سپس اکشن متد AdditionalAuthenticationFactor در حالت Get، این View را نمایش میدهد و در حالت Post، اطلاعات آنرا از کاربر دریافت خواهد کرد:
namespace DNT.IDP.Controllers.Account { public class AccountController : Controller { [HttpGet] public IActionResult AdditionalAuthenticationFactor(string returnUrl, bool rememberLogin) { // create VM var vm = new AdditionalAuthenticationFactorViewModel { RememberLogin = rememberLogin, ReturnUrl = returnUrl }; return View(vm); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> AdditionalAuthenticationFactor( AdditionalAuthenticationFactorViewModel model) { if (!ModelState.IsValid) { return View(model); } // read identity from the temporary cookie var info = await HttpContext.AuthenticateAsync(Startup.TwoFactorAuthenticationScheme); var tempUser = info?.Principal; if (tempUser == null) { throw new Exception("2FA error"); } // ... check code for user if (!await _twoFactorAuthenticationService.IsValidTemporaryCodeAsync(tempUser.GetSubjectId(), model.Code)) { ModelState.AddModelError("code", "2FA code is invalid."); return View(model); } // login the user AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; } // issue authentication cookie for user var user = await _usersService.GetUserBySubjectIdAsync(tempUser.GetSubjectId()); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username)); await HttpContext.SignInAsync(user.SubjectId, user.Username, props); // delete temporary cookie used for 2FA await HttpContext.SignOutAsync(Startup.TwoFactorAuthenticationScheme); if (_interaction.IsValidReturnUrl(model.ReturnUrl) || Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } return Redirect("~/"); }
- فراخوانی HttpContext.SignInAsync با اسکیمای مشخص شده، یک کوکی رمزنگاری شده را در اکشن متد Login ایجاد میکند. اکنون در اینجا با استفاده از متد HttpContext.AuthenticateAsync و ذکر همان اسکیما، میتوانیم به محتوای این کوکی رمزنگاری شده دسترسی داشته باشیم و از طریق آن، Id کاربر را استخراج کنیم.
- اکنون که این Id را داریم و همچنین Code موقتی نیز از طرف کاربر ارسال شدهاست، آنرا به متد IsValidTemporaryCodeAsync که پیشتر در مورد آن توضیح دادیم، ارسال کرده و اعتبارسنجی میکنیم.
- در آخر این کوکی رمزنگاری شده را با فراخوانی متد HttpContext.SignOutAsync، حذف و سپس یک کوکی جدید را بر اساس اطلاعات هویت کاربر، توسط متد HttpContext.SignInAsync ایجاد و ثبت میکنیم تا کاربر بتواند بدون مشکل وارد سیستم شود.
View متناظر با آن نیز در فایل src\IDP\DNT.IDP\Views\Account\AdditionalAuthenticationFactor.cshtml، به صورت زیر تعریف شدهاست تا کد موقتی را به همراه آدرس بازگشت پس از ورود آن، به سمت سرور ارسال کند:
@model AdditionalAuthenticationFactorViewModel <div> <div class="page-header"> <h1>2-Factor Authentication</h1> </div> @Html.Partial("_ValidationSummary") <div class="row"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Input your 2FA code</h3> </div> <div class="panel-body"> <form asp-route="Login"> <input type="hidden" asp-for="ReturnUrl" /> <input type="hidden" asp-for="RememberLogin" /> <fieldset> <div class="form-group"> <label asp-for="Code"></label> <input class="form-control" placeholder="Code" asp-for="Code" autofocus> </div> <div class="form-group"> <button class="btn btn-primary">Submit code</button> </div> </fieldset> </form> </div> </div> </div> </div>
آزمایش برنامه جهت بررسی اعتبارسنجی دو مرحلهای
پس از طی این مراحل، اعتبارسنجی دو مرحلهای در برنامه فعال شدهاست. اکنون برای آزمایش آن، برنامهها را اجرا میکنیم. پس از لاگین، صفحهی زیر نمایش داده میشود:
همچنین کد موقتی این مرحله را نیز در لاگهای برنامه مشاهده میکنید:
پس از ورود آن، کار اعتبارسنجی نهایی آن انجام شده و سپس بلافاصله به برنامهی MVC Client هدایت میشویم.
اضافه کردن اعتبارسنجی دو مرحلهای به قسمت ورود از طریق تامین کنندههای هویت خارجی
دقیقا همین مراحل را نیز به اکشن متد Callback کنترلر ExternalController اضافه میکنیم. در این اکشن متد، تا قسمت کدهای مشخص شدن user آن که از اکانت خارجی وارد شدهاست، با قبل یکی است. پس از آن تمام کدهای لاگین شخص به برنامه را از اینجا حذف و به اکشن متد جدید AdditionalAuthenticationFactor در همین کنترلر منتقل میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.