چرا در C# 9.0 تا این اندازه بر روی سادگی ایجاد اشیاء Immutable تمرکز شدهاست؟
به شیءای Immutable گفته میشود که پس از وهله سازی ابتدایی آن، وضعیت آن دیگر قابل تغییر نباشد. همچنین به کلاسی Immutable گفته میشود که تمام وهلههای ساخته شدهی از آن نیز Immutable باشند. نمونهی یک چنین شیءای را از نگارش 1 دات نت در حال استفاده هستیم: رشتهها. رشتهها در دات نت غیرقابل تغییر هستند و هرگونه تغییری بر روی آنها، سبب ایجاد یک رشتهی جدید (یک شیء جدید) میشود. نوع جدید record نیز به همین صورت عمل میکند.
مزایای وجود Immutability:
- اشیاء Immutable یا غیرقابل تغییر، thread-safe هستند که در نتیجه، برنامه نویسی همزمان و موازی را بسیار ساده میکنند؛ چون چندین thread میتوانند با شیءای کار کنند که دسترسی به آن، تنها read-only است.
- اشیاء Immutable از اثرات جانبی، مانند تغییرات آنها در متدهای مختلف در امان هستند. میتوانید آنها را به هر متدی ارسال کنید و مطمئن باشید که پس از پایان کار، این شیء تغییری نکردهاست.
- کار با اشیاء Immutable، امکان بهینه سازی حافظه را میسر میکنند. برای مثال NET runtime.، هش رشتههای تعریف شدهی در برنامه را در پشت صحنه نگهداری میکند تا مطمئن شود که تخصیص حافظهی اضافی، برای رشتههای تکراری صورت نمیگیرد. نمونهی دیگر آن نمایش حرف "a" در یک ادیتور یا نمایشگر است. زمانیکه یک شیء Immutable حاوی اطلاعات حرف "a"، ایجاد شود، به سادگی میتوان این تک وهله را جهت نمایش هزاران حرف "a" مورد استفادهی مجدد قرار داد، بدون اینکه نگران مصرف حافظهی بالای برنامه باشیم.
- کار با اشیاء Immutable به باگهای کمتری ختم میشود؛ چون همواره امکان تغییر حالت درونی یک شیء، توسط قسمتهای مختلف برنامه، میتواند به باگهای ناخواستهای منتهی شوند.
- Hash listها که در جهت بهبود کارآیی برنامهها بسیار مورد استفاده قرار میگیرند، بر اساس کلیدهایی Immutable قابل تشکیل هستند.
روش تعریف نوعهای جدید record
کلاس سادهی زیر را در نظر بگیرید:
public class User { public string Name { set; get; } }
public record User { public string Name { set; get; } }
var user = new User(); user.Name = "User 1";
روش تعریف دومی نیز در اینجا میسر است (به آن positional record هم میگویند):
public record User(string Name);
برای کار با رکورد دومی که تعریف کردیم باید سازندهی این record را مقدار دهی کرد:
var user = new User("User 1"); // Error: Init-only property or indexer 'User.Name' can only be assigned // in an object initializer, or on 'this' or 'base' in an instance constructor // or an 'init' accessor. [CS9Features]csharp(CS8852) user.Name = "User 1";
نوع جدید record چه اطلاعاتی را به صورت خودکار تولید میکند؟
روش دوم تعریف recordها اگر در نظر بگیریم:
public record User(string Name);
using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using CS9Features; public class User : IEquatable<User> { protected virtual Type EqualityContract { [System.Runtime.CompilerServices.NullableContext(1)] [CompilerGenerated] get { return typeof(User); } } public string Name { get; set/*init*/; } public User(string Name) { this.Name = Name; base..ctor(); } public override string ToString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("User"); stringBuilder.Append(" { "); if (PrintMembers(stringBuilder)) { stringBuilder.Append(" "); } stringBuilder.Append("}"); return stringBuilder.ToString(); } protected virtual bool PrintMembers(StringBuilder builder) { builder.Append("Name"); builder.Append(" = "); builder.Append((object?)Name); return true; } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator !=(User? r1, User? r2) { return !(r1 == r2); } [System.Runtime.CompilerServices.NullableContext(2)] public static bool operator ==(User? r1, User? r2) { return (object)r1 == r2 || (r1?.Equals(r2) ?? false); } public override int GetHashCode() { return EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Name); } public override bool Equals(object? obj) { return Equals(obj as User); } public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name); } public virtual User <Clone>$() { return new User(this); } protected User(User original) { Name = original.Name; } public void Deconstruct(out string Name) { Name = this.Name; } }
- recordها هنوز هم در اصل همان classهای استاندارد #C هستند (یعنی در اصل reference type هستند).
- این کلاس به همراه یک سازنده و یک خاصیت init-only است (بر اساس تعاریف ما).
- متد ToString آن بازنویسی شدهاست تا اگر آنرا بر روی شیء حاصل، فراخوانی کردیم، به صورت خودکار نمایش زیبایی را از محتوای آن ارائه دهد.
- این کلاس از نوع <IEquatable<User است که امکان مقایسهی اشیاء record را به سادگی میسر میکند. برای این منظور متدهای GetHashCode و Equals آن به صورت خودکار بازنویسی و تکمیل شدهاند (یعنی مقایسهی آن شبیه به value-type است).
- این کلاس امکان clone کردن اطلاعات جاری را مهیا میکند.
- همچنین به همراه یک متد Deconstruct هم هست که جهت انتساب خواص تعریف شدهی در آن، به یک tuple مفید است.
بنابراین یک رکورد به همراه قابلیتهایی است که سالها در زبان #C وجود داشتهاند و شاید ما به سادگی حاضر به تشکیل و تکمیل آنها نمیشدیم؛ اما اکنون کامپایلر زحمت کدنویسی خودکار آنها را متقبل میشود!
ساخت یک وهلهی جدید از یک record با clone کردن آن
اگر به کدهای حاصل از دیکامپایل فوق دقت کنید، یک قسمت جدید clone هم با syntax خاصی در آن ظاهر شدهاست:
public virtual User <Clone>$() { return new User(this); }
public record User(string Name, int Age);
var user1 = new User("User 1", 21);
var user2 = user1 with { Age = 31 };
مقایسهی نوعهای record
در کدهای حاصل از دیکامپایل فوق، قسمت عمدهای از آن به تکمیل اینترفیس <IEquatable<User پرداخته شده بود. به همین جهت اکنون دو رکورد با مقادیر خواص یکسانی را ایجاد میکنیم:
var user1 = new User("User 1", 21); var user2 = new User("User 1", 21);
Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> True user1 == user2 -> True
- زمانیکه عملگر == را بر روی شیء user1 و user2 اعمال میکنیم، اگر User، از نوع کلاس معمولی باشد، حاصل آن false خواهد بود؛ چون این دو، به یک مکان از حافظه اشاره نمیکنند، حتی با اینکه مقادیر خواص هر دو شیء یکی است.
- اما اگر به قطعه کد دیکامپایل شده دقت کنید، در یک رکورد که هر چند در اصل یک کلاس است، حتی عملگر == نیز بازنویسی شدهاست تا در پشت صحنه همان متد Equals را فراخوانی کند و این متد با توجه به پیاده سازی اینترفیس <IEquatable<User، اینبار دقیقا مقادیر خواص رکورد را یک به یک مقایسه کرده و نتیجهی حاصل را باز میگرداند:
public virtual bool Equals(User? other) { return (object)other != null && EqualityContract == other!.EqualityContract && EqualityComparer<string>.Default.Equals(Name, other!.Name) && EqualityComparer<int>.Default.Equals(Age, other!.Age); }
یک نکته: بازنویسی عملگر == در SDK نگارش rc2 فعلی رخدادهاست و در نگارشهای قبلی preview، اینگونه نبود.
امکان ارثبری در recordها
دو رکورد زیر را در نظر بگیرید که اولی به همراه Name است و نمونهی مشتق شدهی از آن، خاصیت init-only سن را نیز به همراه دارد:
public record User { public string Name { get; init; } public User(string name) { Name = name; } } public record UserWithAge : User { public int Age { get; init; } public UserWithAge(string name, int age) : base(name) { Age = age; } }
var user1 = new User("User 1"); var user2 = new UserWithAge("User 1", 21); Console.WriteLine("user1.Equals(user2) -> {0}", user1.Equals(user2)); Console.WriteLine("user1 == user2 -> {0}", user1 == user2);
user1.Equals(user2) -> False user1 == user2 -> False
امکان تعریف ارثبری رکوردها به صورت زیر نیز وجود دارد و الزاما نیازی به روش تعریف کلاس مانند آنها، مانند مثال فوق نیست:
public abstract record Food(int Calories); public record Milk(int C, double FatPercentage) : Food(C);
رکوردها متد ToString را بازنویسی میکنند
در مثال قبلی اگر یک ToString را بر روی اشیاء تشکیل شده فراخوانی کنیم:
Console.WriteLine(user1.ToString()); Console.WriteLine(user2.ToString());
User { Name = User 1 } UserWithAge { Name = User 1, Age = 21 }
امکان استفادهی از Deconstruct در رکوردها
دو روش برای تعریف رکوردها وجود دارند؛ یکی شبیه به تعریف کلاسها است و دیگری تعریف یک سطری، که positional record نیز نامیده میشود:
public record Person(string Name, int Age);
public void Deconstruct(out string Name, out int Age) { Name = this.Name; Age = this.Age; }
var (name, age) = new Person("User 1", 21);
امکان استفادهی از نوعهای record در ASP.NET Core 5x
سیستم model binding در ASP.NET Core 5x، از نوعهای record نیز پشتیبانی میکند؛ یک مثال:
public record Person([Required] string Name, [Range(0, 150)] int Age); public class PersonController { public IActionResult Index() => View(); [HttpPost] public IActionResult Index(Person person) { // ... } }
پرسش و پاسخ
آیا نوعهای record به صورت value type معرفی میشوند؟
پاسخ: خیر. رکوردها در اصل reference type هستند؛ اما از لحاظ مقایسه، شبیه به value types عمل میکنند.
آیا میتوان در یک کلاس، خاصیتی از نوع رکورد را تعریف کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان در رکوردها، از struct و یا کلاسها جهت تعریف خواص استفاده کرد؟
پاسخ: بله. از این لحاظ محدودیتی وجود ندارد.
آیا میتوان از واژهی کلیدی with با کلاسها و یا structها استفاده کرد؟
پاسخ: خیر. این واژهی کلیدی در C# 9.0 مختص به رکوردها است.
آیا رکوردها به صورت پیشفرض Immutable هستند؟
پاسخ: اگر آنها را به صورت positional records تعریف کنید، بله. چون در این حالت خواص تشکیل شدهی توسط آنها از نوع init-only هستند. در غیراینصورت، میتوان خواص غیر init-only را نیز به تعریف رکوردها اضافه کرد.
ترکیب کامپوننتها
در ادامه، همان برنامهی تا قسمت 5 را که کار نمایش یک counter را انجام میدهد، تکمیل میکنیم. در این برنامه اگر به فایل index.js دقت کنید، کار رندر تک کامپوننت Counter را انجام میدهیم:
ReactDOM.render(<Counter />, document.getElementById("root"));
برای نمایش این مفهوم، کامپوننت جدید src\components\counters.jsx را ایجاد میکنیم. قصد داریم در این کامپوننت، لیستی از کامپوننتهای Counter را رندر کنیم. سپس در index.js، بجای رندر کامپوننت Counter، کامپوننت جدید Counters را رندر میکنیم. به این ترتیب درخت کامپوننتهای برنامه، در سطح بالایی خودش از کامپوننت Counters شروع میشود و سپس فرزندان آنرا کامپوننتهای Counter تشکیل میدهند. به همین جهت فایل index.js را به صورت زیر ویرایش میکنیم تا به کامپوننت Counters اشاره کند:
import Counters from "./components/counters"; ReactDOM.render(<Counters />, document.getElementById("root"));
import React, { Component } from "react"; import Counter from "./counter"; class Counters extends Component { state = {}; render() { return ( <div> <Counter /> <Counter /> <Counter /> <Counter /> </div> ); } } export default Counters;
در مرحلهی بعد، بجای رندر و درج دستی این کامپوننتها، آرایهای از اشیاء counter را ایجاد کرده و سپس آنها را توسط متد Array.map رندر میکنیم:
import React, { Component } from "react"; import Counter from "./counter"; class Counters extends Component { state = { counters: [ { id: 1, value: 0 }, { id: 2, value: 0 }, { id: 3, value: 0 }, { id: 4, value: 0 } ] }; render() { return ( <div> {this.state.counters.map(counter => ( <Counter key={counter.id} /> ))} </div> ); } } export default Counters;
ارسال دادهها به کامپوننتها
مشکل! مقدار value هر شیء شمارشگر تعریف شده، به کامپوننتهای مرتبط رندر شده اعمال نشدهاست. برای مثال اگر value اولین شیء را به 4 تغییر دهیم، هنوز هم این کامپوننت با همان مقدار صفر شروع به کار میکند. برای رفع این مشکل، به همان روشی که ویژگی key کامپوننت Counter را مقدار دهی کردیم، میتوان ویژگیهای سفارشی دیگری را تعریف و مقدار دهی کرد:
render() { return ( <div> {this.state.counters.map(counter => ( <Counter key={counter.id} value={counter.value} selected={true} /> ))} </div> );
class Counter extends Component { state = { count: 0 }; render() { console.log("props", this.props); //...
خاصیت this.props، یک شیء سادهی جاوا اسکریپتی است و شامل تمام ویژگیهایی میباشد که ما در کامپوننت Counters برای هر کدام از کامپوننتهای Counter رندر شدهی توسط آن، تعریف کردیم. برای نمونه دو ویژگی جدید value و selected را که به تعاریف المانهای Counter در کامپوننت Counters اضافه کردیم، در اینجا به همراه مقادیر منتسب به آنها، قابل مشاهده هستند. البته در این خروجی، key را ملاحظه نمیکنید؛ چون هدف اصلی آن، معرفی یکتای المانها در DOM مجازی React است.
بنابراین اکنون میتوان به value تنظیم شدهی در کامپوننت Counters به صورت this.props.value در کامپوننت Counter دسترسی یافت و سپس از آن جهت مقدار دهی اولیهی counter استفاده کرد.
class Counter extends Component { state = { count: this.props.value };
یک نکته: در اینجا selected={true} را داریم. اگر مقدار آنرا حذف کنیم، یعنی selected تنها درج شود، مقدار آن، همان true دریافت خواهد شد.
تعریف فرزند برای المانهای کامپوننتها
ویژگیهای اضافه شدهی به تعاریف المانهای کامپوننتها، توسط خاصیت this.props، به هر کدام از آن کامپوننتها منتقل میشوند. این خاصیت props، یک خاصیت ویژه را به نام children، نیز دارا است و از آن برای دسترسی به المانهای تعریف شدهی بین تگهای یک المان اصلی استفاده میشود:
render() { return ( <div> {this.state.counters.map(counter => ( <Counter key={counter.id} value={counter.value} selected={true}> <h4>Counter #{counter.id}</h4> </Counter> ))} </div> ); }
یک نمونه مثال واقعی این قابلیت، امکان تعریف محتوای دیالوگ باکسها، توسط استفاده کنندهی از آن است.
روش دیباگ برنامههای React
افزونهی مفید React developer tools را میتوانید برای مرورگرهای کروم و فایرفاکس، دریافت و نصب کنید. برای نمونه پس از نصب آن در مرورگر کروم، یک برگهی جدید به لیست برگههای کنسول توسعه دهندگان آن اضافه میشود:
همانطور که مشاهده کنید، درخت کامپوننتهای برنامه را در برگهی جدید Components، میتوان مشاهده کرد. در اینجا با انتخاب هر کدام از فرزندان این درخت، مشخصات آن نیز مانند props و state، در کنار صفحه ظاهر میشوند. همچنین در بالای همین قسمت، 4 آیکن مشاهدهی سورس، مشاهدهی DOM و یا لاگ کردن جزئیات شیء کامپوننت انتخابی در کنسول هم درج شدهاند:
که برای نمونه چنین خروجی را لاگ میکند:
بررسی تفاوتهای خواص props و state
در کامپوننت Counter، از props برای مقدار دهی اولیهی state استفاده میکنیم:
class Counter extends Component { state = { count: this.props.value };
- props حاوی اطلاعاتی است که به یک کامپوننت ارسال میکنیم؛ اما state حاوی اطلاعاتی است که مختص به آن کامپوننت بوده و private است. یعنی سایر کامپوننتها نمیتوانند به state کامپوننت دیگری دسترسی پیدا کنند. برای مثال در کامپوننت Counters، تمام attributes سفارشی تنظیم شدهی بر روی تعاریف المانهای کامپوننت Counter، جزئی از اطلاعات props خواهند بود. در اینجا نمیتوان به state کامپوننت مدنظری دسترسی یافت و آنرا مقدار دهی کرد. به همین ترتیب state کامپوننت Counters نیز در سایر کامپوننتها قابل دسترسی نیست.
- همچنین باید درنظر داشت که props، در مقایسه با state، فقط خواندنی است. به عبارتی مقدار ورودی به یک کامپوننت را داخل آن کامپوننت نمیتوان تغییر داد. برای مثال سعی کنید در داخل متد رویدادگردان کلیک موجود در کامپوننت Counter، مقدار this.props.value را به صفر تنظیم کنید. در این حالت با کلیک بر روی دکمهی Increment، بلافاصله خطای readonly بودن خواص شیء منتسب به props را دریافت میکنیم. در اینجا اگر نیاز است این مقدار را داخل کامپوننت تغییر دهیم، باید ابتدا این مقدار را دریافت کرده و سپس آنرا داخل state قرار دهیم. پس از آن امکان ویرایش اطلاعات منتسب به state، داخل یک کامپوننت وجود خواهد داشت.
صدور و مدیریت رخدادها
در ادامه میخواهیم در کنار هر دکمهی Increment کامپوننت شمارشگر، یک دکمهی Delete هم قرار دهیم:
مشکل! اگر کد مدیریتی handleDelete را در کامپوننت Counter قرار دهیم، چگونه باید به لیست آرایهی اشیاء counters والد آن، یعنی کامپوننت Counters که سبب رندر شدن کامپوننتهای شمارشگر شده (state = { counters: [ ] })، دسترسی یافت و شیءای را از آن حذف کرد؟ در React، کامپوننتی که state ای را تعریف میکند، باید کامپوننتی باشد که قرار است آنرا تغییر دهد و اطلاعات state هر کامپوننت، صرفا متعلق به آن کامپوننت بوده و جزو اطلاعات خصوصی آن است. بنابراین مدیریت حذف و یا افزودن کامپوننتها در لیست نمایش داده شده، باید جزو وظایف کامپوننت Counters باشد و نه Counter.
برای حل این مشکل، کامپوننت Counter تعریف شده (کامپوننت فرزند) باید سبب بروز رخداد onDelete شود تا کامپوننت Counters (کامپوننت والد)، آنرا توسط متد handleDelete مدیریت کند. بنابراین ابتدا به کامپوننت Counters (کامپوننت والد) مراجعه کرده و متد رویدادگردان handleDelete را به آن اضافه میکنیم:
handleDelete = () => { console.log("handleDelete called."); };
<Counter key={counter.id} value={counter.value} selected={true} onDelete={this.handleDelete} />
<button onClick={this.props.onDelete} className="btn btn-danger btn-sm m-2" > Delete </button>
اکنون اگر برنامه را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر بر روی دکمهی Delete کلیک کنیم، پیام «handleDelete called» در کنسول توسعه دهندگان مرورگر لاگ میشود. به این ترتیب کامپوننت فرزند سبب بروز رخدادی شده و والد آن، این رخداد را مدیریت میکند.
به روز رسانی state
تا اینجا دکمهی Delete فرزند، به متد handleDelete والد متصل شدهاست. مرحلهی بعد، پیاده سازی واقعی حذف یک المان از DOM مجازی و به روز رسانی state است. برای اینکار ابتدا به رخدادگردان onClick، در کامپوننت شمارشگر، مراجعه کرده و id دریافتی را به سمت والد ارسال میکنیم:
onClick={() => this.props.onDelete(this.props.id)}
<Counter key={counter.id} value={counter.value} selected={true} onDelete={this.handleDelete} id={counter.id} />
handleDelete = counterId => { console.log("handleDelete called.", counterId); const counters = this.state.counters.filter( counter => counter.id !== counterId ); this.setState({ counters }); // = this.setState({ counters: counters }); };
البته پیاده سازی ما تا به اینجا بدون مشکل کار میکند، اما به ازای هر خاصیت counter، یک ویژگی جدید را به تعریف المان مرتبط اضافه کردهایم که در طول زمان بیش از اندازه طولانی خواهد شد. برای رفع این مشکل، خود شیء counter را به صورت یک ویژگی جدید به کامپوننت مرتبط با آن ارسال میکنیم. به این ترتیب اگر در آینده خاصیتی را به این شیء اضافه کردیم، دیگر نیازی نیست تا آنرا به صورت دستی و مجزا تعریف کنیم. به همین جهت ابتدا تعریف المان Counter را به صورت زیر خلاصه میکنیم که در آن ویژگی جدید counter، حاوی کل شیء counter است:
<Counter key={counter.id} counter={counter} onDelete={this.handleDelete} />
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-07.zip
به طور خلاصه میتوانید امکانات متعدد این پلاگین را در زیر مشاهده کنید:
- صفحه بندی دادهها با تعداد رکوردهای قابل تغییر در هر صفحه (variable length pagination)
- فیلتر کردن دادههای بایند شده به جدول (on-the-fly filtering)
- مرتب سازی دادهها بر اساس ستونهای مختلف با قابلیت تشخیص نوع داده ستون (Multi-column sorting with data type detection)
- تغییر اندازه ستونها به صورت هوشمند (Smart handling of column widths)
- نمایش دادهها در جدول از اکثر data sourceها (DOM، یک آرایه جاوا اسکریپتی، یک فایل، یا با استفاده از پردازش سمت سروری (سی شارپ، php و غیره) )
- قابلیت جهانی شدن یا منطبق شدن با زبانهای مختلف دنیا (Fully Internationalisable)
- قابلیت تعویض theme آن با استفاده از jQuery UI ThemeRoller
- وجود داشتن 2900 آزمون واحد برای آن (backed by a suite of 2900 unit test)
- وجود داشتن پلاگینهای متعدد برای آن
- و رایگان بودن آن
برای استفاده از این پلاگین ابتدا به اینجا مراجعه کرده و آنرا به همراه مثالهای آن که در یک فایل فشرده هستند را دانلود کنید. بعد از دانلود و خارج کردن فایل دانلودی از حالت فشرده، وارد پوشه examples از آن که بشوید میتوانید مثالهای متعدد در رابطه با این پلاگین را مشاهده نمائید.
مثالهای این پلاگین یکی از بهترین منابع یادگیری آن هستند. در این سری از مقالات هم از روی همین مثالها پیش میرویم. برای این کار، بعد از مراجعه به پوشه examples فایل index.html را باز کنید و مثال اول را (Zero Configuration) کلیک کنید.
نتیجه حاصل از اجرای مثال Zero Configuration چیزی شبیه تصویر زیر است:
تصویر را شماره گزاری کرده ام تا بتوانم راحتتر آنرا برایتان تشریح کنم.
- دادههای درون جدول (10 تای اول) که در قسمت tbody جدول قرار دارند
- قسمت thead جدول
- قسمت tfoot جدول
- اندازه صفحه (page size)
- کادر جستجو که در کلیه ستونهای جدول جستجویی را انجام میدهد و دادهها بر اساس آن فیلتر میشوند.
- قابلیت مرتب سازی رکوردها بر اساس یک ستون خاص به صورت صعودی یا نزولی
- اطلاعات مربوط به رکوردهای جاری و تعداد کل رکوردها
- قابلیت تغییر صفحه با دکمههای previous و next
تشریح مثال Zero Configuration :
برای استفاده از این پلاگین، باید ارجاعی به کتابخانه jquery و نیز فایل jquery.dataTables.js وجود داشته باشد. این دو فایل در زیر پوشه media/js قرار گرفته اند.
<script type="text/javascript" language="javascript" src="../../media/js/jquery.js"></script> <script type="text/javascript" language="javascript" src="../../media/js/jquery.dataTables.js"></script>
<style type="text/css" title="currentStyle"> @import "../../media/css/demo_page.css"; @import "../../media/css/demo_table.css"; </style>
این نکته هم جا نمونه که برای اعمال شدن پلاگین DataTables به یک جدول که به طور مثال id جدول example هست، به صورت زیر عمل میکنیم:
$(document).ready(function() { $('#example').dataTable(); } );
اگر پیشتر سابقهی کار کردن با ASP.NET MVC را ندارید، نیاز است «15 مورد» ابتدایی مطالب ASP.NET MVC سایت را پیش از ادامهی این سری مطالعه کنید؛ از این جهت که این سری از مطالب «ارتقاء» نام دارند و نه «بازنویسی مجدد». دراینجا بیشتر تفاوتها و روشهای تبدیل کدهای قدیمی، به جدید را بررسی خواهیم کرد؛ تا اینکه بخواهیم تمام مطالبی را که وجود دارند از صفر بازنویسی کنیم.
فعال سازی ASP.NET MVC
تا اینجا خروجی برنامه را صرفا توسط میان افزار app.Run نمایش دادیم. اما در نهایت میخواهیم یک برنامهی ASP.NET MVC را برفراز ASP.NET Core 1.0 اجرا کنیم و این قابلیت نیز به صورت پیش فرض غیرفعال است. برای فعال سازی آن نیاز است ابتدا بستهی نیوگت آنرا نصب کرد. سپس سرویسهای مرتبط با آنرا ثبت و معرفی نمود و در آخر میان افزار خاص آنرا فعال کرد.
نصب وابستگیهای ASP.NET MVC
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.Mvc را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.Mvc": "1.0.0" },
تنظیم سرویسها و میان افزارهای ASP.NET MVC
پس از نصب بستهی نیوگت ASP.NET MVC، دو تنظیم ذیل در فایل آغازین برنامه، برای شروع به کار با ASP.NET MVC کفایت میکنند:
الف) ثبت یکجای سرویسهای ASP.NET MVC
public void ConfigureServices(IServiceCollection services) { services.AddMvc();
ب) معرفی میان افزار ASP.NET MVC
public void Configure(IApplicationBuilder app) { app.UseFileServer(); app.UseMvcWithDefaultRoute();
در اینجا دو متد UseMvc و UseMvcWithDefaultRoute را داریم. اولی، امکان تعریف مسیریابیهای سفارشی را میسر میکند و دومی به همراه یک مسیریابی پیش فرض است.
افزودن اولین کنترلر برنامه و معرفی POCO Controllers
در ویژوال استودیو بر روی نام پروژه کلیک راست کرده و پوشهی جدیدی را به نام کنترلر اضافه کنید (تصویر فوق). سپس به این پوشه کلاس جدید HomeController را با این محتوا اضافه کنید:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
سپس برنامه را اجرا کنید. این خروجی باید قابل مشاهده باشد:
اگر با نگارشهای قبلی ASP.NET MVC کار کرده باشید، تفاوت این کنترلر با آنها، در عدم ارث بری آن از کلاس پایهی Controller است. به همین جهت به آن POCO Controller نیز میگویند (plain old C#/CLR object).
در ASP.NET Core، همینقدر که یک کلاس public غیر abstract را که نامش به Controller ختم شود، داشته باشید و این کلاس در اسمبلی باشد که ارجاعی را به وابستگیهای ASP.NET MVC داشته باشد، به عنوان یک کنترلر معتبر شناخته شده و مورد استفاده قرار خواهد گرفت. در نگارشهای قبلی، شرط ارث بری از کلاس پایه Controller نیز الزامی بود؛ اما در اینجا خیر. هدف از آن نیز کاهش سربارهای وهله سازی یک کنترلر است. اگر صرفا میخواهید یک شیء را به صورت JSON بازگشت دهید، شاید وهله سازی یک کلاس ساده، بسیار بسیار سریعتر از نمونه سازی یک کلاس مشتق شدهی از Controller، به همراه تمام وابستگیهای آن باشد.
البته هنوز هم مانند قبل، کنترلرهای مشتق شدهی از کلاس پایهی Controller قابل تعریف هستند:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public IActionResult Index() { return Content("Hello from DNT!"); } } }
تفاوت دیگری را که ملاحظه میکنید، خروجی IActionResult بجای ActionResult نگارشهای قبلی است. در اینجا هنوز هم ActionResult را میتوان بکار برد و اینبار ActionResult، پیاده سازی پیش فرض اینترفیس IActionResult است.
و اگر بخواهیم در POCO Controllers شبیه به return Content فوق را پیاده سازی کنیم، نیاز است تا تمام جزئیات را از ابتدا پیاده سازی کنیم (چون کلاس پایه و ساده ساز Controller در اختیار ما نیست):
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class HomeController { [ActionContext] public ActionContext ActionContext { get; set; } public HttpContext HttpContext => ActionContext.HttpContext; public string Index() { return "Running a POCO controller!"; } public IActionResult About() { return new ContentResult { Content = "Hello from DNT!", ContentType = "text/plain; charset=utf-8" }; } } }
به علاوه در اینجا نحوهی دسترسی به HttpContext را هم مشاهده میکنید. ویژگی ActionContext سبب تزریق اطلاعات آن به کنترلر جاری شده و سپس از طریق آن میتوان به HttpContext و تمام قابلیتهای آن دسترسی یافت.
اینجا است که میتوان میزان سبکی و سریعتر بودن POCO Controllers را احساس کرد. شاید در کنترلری نیاز به این وابستگیها نداشته باشید. اما زمانیکه کنترلری از کلاس پایهی Controller مشتق میشود، تمام این وابستگیها را به صورت پیش فرض و حتی در صورت عدم استفاده، در اختیار خواهد داشت و این در اختیار داشتن یعنی وهله سازی شدن تمام وابستگیهای مرتبط با شیء پایهی Controller. به همین جهت است که POCO Controllers بسیار سبکتر و سریعتر از کنترلرهای متداول مشتق شدهی از کلاس پایهی Controller عمل میکنند.
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده میگیرد که به آن «single source of truth» نیز گفته میشود. البته هرچند میتوان کامپوننتهایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آنرا در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخدادهایی هستند که در اینجا اکشن نامیده میشوند. یک اکشن شیءای است که بیان میکند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء میتوان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیدهاست، تغییر میدهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافتهی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه میتوان چندین تابع Reducer را بر اساس نوعهای اکشنهای ویژهای، پیاده سازی کرد. سپس با ترکیب آنها، یک شیء rootReducer ایجاد میشود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میانافزارهای تعریف شده مانند Thunk، در کنار هم قرار میدهد و امکان dispatch اکشنها را میسر میکند.
- اکنون برای استفادهی از Redux در یک برنامهی React، نیاز است کامپوننت ریشهی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمتهای مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننتهای اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده میشود، توسط متد connect کتابخانهی react-redux محصور میشوند. به این ترتیب این کامپوننتها میتوانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).
از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهمترینهای آنها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننتهای تابعی React اکنون میتوانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانههای نوشته شدهی برای React نیز شروع به انطباق خود با این الگوی جدید کردهاند؛ برای مثال کتابخانهی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننتها و دربرگیری آنها باشند که در ادامه جزئیات آنها را بررسی خواهیم کرد.
بررسی useSelector Hook
useSelector Hook که توسط کتابخانهی react-redux ارائه میشود، معادل بسیار نزدیک تابع mapStateToProps مورد استفادهی در متد connect است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux"; import Posts from "../components/Posts"; const mapStateToProps = state => { console.log("PostsContainer->mapStateToProps", state); return { ...state.postsReducer }; }; export default connect(mapStateToProps)(Posts);
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت میکرد:
const Posts = ({ posts, loading, error }) => { return ( // ...
اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر میدهیم:
import { useSelector } from "react-redux"; // ... const HooksPosts = () => { const { posts, loading, error } = useSelector(state => state.postsReducer); return ( // ...
یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز میتوان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.
این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، میتوان دربرگیرندهی کامپوننت Posts را:
import PostsContainer from "./containers/Posts"; function App() { return ( <main className="container"> <PostsContainer /> </main> ); }
import HooksPosts from "./components/HooksPosts"; function App() { return ( <main className="container"> <HooksPosts /> </main> ); }
بررسی useDispatch Hook
تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحلهی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرندهی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux"; import { fetchPostsAsync } from "../actions"; import FetchPosts from "../components/FetchPosts"; const mapDispatchToProps = { fetchPostsAsync }; export default connect(null, mapDispatchToProps)(FetchPosts);
const FetchPosts = ({ fetchPostsAsync }) => {
import React from "react"; import { useDispatch } from "react-redux"; import { fetchPostsAsync } from "../actions"; const HooksFetchPosts = () => { const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-header text-center"> <button className="btn btn-primary" onClick={() => dispatch(fetchPostsAsync())} > Fetch Posts </button> </div> </section> ); }; export default HooksFetchPosts;
با این تغییر نیز میتوان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین میشد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts"; import HooksPosts from "./components/HooksPosts"; // ... function App() { return ( <main className="container"> <HooksFetchPosts /> <HooksPosts /> </main> ); }
کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کنندهی آنرا که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; import Counter from "../components/counter"; const mapStateToProps = (state, ownProps) => { console.log("CounterContainer->mapStateToProps", { state, ownProps }); return { count: state.counterReducer.count }; }; const mapDispatchToProps = { incrementValue, decrementValue }; export default connect(mapStateToProps, mapDispatchToProps)(Counter);
class Counter extends Component { render() { console.log("Counter->props", this.props); const { //counterReducer: { count }, count, incrementValue, decrementValue } = this.props;
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( // ...
- اینبار دو action creator مورد استفادهی در متدهای + و - را از ماژول action دریافت کردهایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال میشود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی میکنیم، توسط کامپوننت دیگری محصور نشدهاست. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت میگیرد:
import HooksCounter from "./components/HooksCounter"; //... function App() { const prop1 = 123; return ( <main className="container"> <HooksCounter prop1={prop1} /> </main> ); }
import React from "react"; import { useDispatch, useSelector } from "react-redux"; import { decrementValue, incrementValue } from "../actions"; const HooksCounter = ({ prop1 }) => { const { count } = useSelector(state => { console.log("HooksCounter->useSelector", { state, prop1 }); return { count: state.counterReducer.count }; }); const dispatch = useDispatch(); return ( <section className="card mt-5"> <div className="card-body text-center"> <span className="badge m-2 badge-primary">{count}</span> </div> <div className="card-footer"> <div className="d-flex justify-content-center align-items-center"> <button className="btn btn-secondary btn-sm" onClick={() => dispatch(incrementValue())} > + </button> <button className="btn btn-secondary btn-sm m-2" onClick={() => dispatch(decrementValue())} > - </button> <button className="btn btn-danger btn-sm">Reset</button> </div> </div> </section> ); }; export default HooksCounter;
مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننتهای برنامه افزایش یافتهاست!
برنامهی جاری را پس از این تغییرات اجرا کنید. با هر بار کلیک بر روی دکمهی fetch posts، حتی کامپوننت شمارشگر درج شدهی در صفحه که ربطی به آن ندارد نیز رندر مجدد میشود! چرا؟ (این مورد را با مشاهدهی کنسول توسعه دهندگان مرورگر میتوانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننتها، یک console.log قرار داده شدهاست)
زمانیکه اکشنی dispatch میشود، useSelector hook با استفاده از مقایسهی ارجاعات اشیاء (strict === reference check)، کار مقایسهی مقدار قبلی و مقدار جدید را انجام میدهد. اگر اینها متفاوت باشند، کامپوننت را مجبور به رندر مجدد میکند. این مورد مهمترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسهی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده میکند. اما این مقایسهها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسهی خاصیت به خاصیت شیء بازگشت داده شده، با نمونهی قبلی است. اما زمانیکه از useSelector hook استفاده میکنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننتها. به همین جهت در این حالت تعداد بار رندر کامپوننتها افزایش یافتهاست، چون خروجی reducerهای تعریف شدهی در برنامه، همیشه یک شیء جدید را بازگشت میدهند.
برای رفع این مشکل میتوان از پارامتر دوم متد useSelector که روش مقایسهی اشیاء را مشخص میکند، استفاده کرد:
import React from "react"; import { shallowEqual, useSelector } from "react-redux"; import Post from "./Post"; const HooksPosts = () => { const { posts, loading, error } = useSelector( state => state.postsReducer, shallowEqual ); console.log("render HooksPosts"); return ( // ...
با اضافه کردن پارامتر shallowEqual به کامپوننتهای HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمهی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمیدهد.
یک نکته: روش دیگر مشاهدهی تعداد بار رندر شدن کامپوننتها، استفاده از افزونهی react dev tools و مراجعه به برگهی profiler آن است. روی دکمهی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجهی نهایی نمایش داده شود.
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
معرفی قالبهای جدید شروع پروژههای Blazor در دات نت 8 به همراه قسمت Identity
در قسمت دوم این سری، با قالبهای جدید شروع پروژههای Blazor 8x آشنا شدیم و هدف ما در آنجا بیشتر بررسی حالتهای مختلف رندر Blazor در دات نت 8 بود. تمام این قالبها به همراه یک سوئیچ دیگر هم به نام auth-- هستند که توسط آن و با مقدار دهی Individual که به معنای Individual accounts است، میتوان کدهای پیشفرض و ابتدایی Identity UI جدید را نیز به قالب در حال ایجاد، به صورت خودکار اضافه کرد؛ یعنی به صورت زیر:
اجرای قسمتهای تعاملی برنامه بر روی سرور؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Server --auth Individual
اجرای قسمتهای تعاملی برنامه در مرورگر، توسط فناوری وباسمبلی؛ به همراه کدهای Identity:
dotnet new blazor --interactivity WebAssembly --auth Individual
برای اجرای قسمتهای تعاملی برنامه، ابتدا حالت Server فعالسازی میشود تا فایلهای WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده میکند؛ به همراه کدهای Identity:
dotnet new blazor --interactivity Auto --auth Individual
فقط از حالت SSR یا همان static server rendering استفاده میشود (این نوع برنامهها تعاملی نیستند)؛ به همراه کدهای Identity:
dotnet new blazor --interactivity None --auth Individual
و یا توسط پرچم all-interactive--، که interactive render mode را در ریشهی برنامه قرار میدهد؛ به همراه کدهای Identity:
dotnet new blazor --all-interactive --auth Individual
یک نکته: بانک اطلاعاتی پیشفرض مورد استفادهی در این نوع پروژهها، SQLite است. برای تغییر آن میتوانید از پرچم use-local-db-- هم استفاده کنید تا از LocalDB بجای SQLite استفاده کند.
Identity UI جدید Blazor در دات نت 8، یک بازنویسی کامل است
در نگارشهای قبلی Blazor هم امکان افزودن Identity UI، به پروژههای Blazor وجود داشت (اطلاعات بیشتر)، اما ... آن پیاده سازی، کاملا مبتنی بر Razor pages بود. یعنی کاربر، برای کار با آن مجبور بود از فضای برای مثال Blazor Server خارج شده و وارد فضای جدید ASP.NET Core شود تا بتواند، Identity UI نوشته شدهی با صفحات cshtml. را اجرا کند و به اجزای آن دسترسی پیدا کند (یعنی عملا آن قسمت UI اعتبارسنجی، Blazor ای نبود) و پس از اینکار، مجددا به سمت برنامهی Blazor هدایت شود؛ اما ... این مشکل در دات نت 8 با ارائهی صفحات SSR برطرف شدهاست.
همانطور که در قسمت قبل نیز بررسی کردیم، صفحات SSR، طول عمر کوتاهی دارند و هدف آنها تنها ارسال یک HTML استاتیک به مرورگر کاربر است؛ اما ... دسترسی کاملی را به HttpContext برنامهی سمت سرور دارند و این دقیقا چیزی است که زیر ساخت Identity، برای کار و تامین کوکیهای مورد نیاز خودش، احتیاج دارد. صفحات Identity UI از یک طرف از HttpContext برای نوشتن کوکی لاگین موفقیت آمیز در مرورگر کاربر استفاده میکنند (در این سیستم، هرجائی متدهای XyzSignInAsync وجود دارد، در پشت صحنه و در پایان کار، سبب درج یک کوکی اعتبارسنجی و احراز هویت در مرورگر کاربر نیز خواهد شد) و از طرف دیگر با استفاده از میانافزارهای اعتبارسنجی و احراز هویت ASP.NET Core، این کوکیها را به صورت خودکار پردازش کرده و از آنها جهت ساخت خودکار شیء User قابل دسترسی در این صفحات (شیء context.User.Identity@)، استفاده میکنند.
به همین جهت تمام صفحات Identity UI ارائه شده در Blazor 8x، از نوع SSR هستند و اگر در آنها از فرمی استفاده شده، دقیقا همان فرمهای تعاملی است که در قسمت چهارم این سری بررسی کردیم. یعنی صرفا بخاطر داشتن یک فرم، ضرورتی به استفادهی از جزایر تعاملی Blazor Server و یا Blazor WASM را ندیدهاند و اینگونه فرمها را بر اساس مدل جدید فرمهای تعاملی Blazor 8x پیاده سازی کردهاند. بنابراین این صفحات کاملا یکدست هستند و از ابتدا تا انتها، به صورت یکپارچه بر اساس مدل SSR کار میکنند (که در اصل خیلی شبیه به Razor pages یا Viewهای MVC هستند) و جزیره، جزیرهای، طراحی نشدهاند.
روش دسترسی به HttpContext در صفحات SSR
اگر به کدهای Identity UI قالب آغازین یک پروژه که در ابتدای بحث روش تولید آنها بیان شد، مراجعه کنید، استفاده از یک چنین «پارامترهای آبشاری» را میتوان مشاهده کرد:
@code { [CascadingParameter] public HttpContext HttpContext { get; set; } = default!;
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
در کدهای Identity UI ارائه شده، از این CascadingParameter برای مثال جهت خروج از برنامه (HttpContext.SignOutAsync) و یا دسترسی به اطلاعات هدرهای رسید (if (HttpMethods.IsGet(HttpContext.Request.Method)))، دسترسی به سرویسها (()<HttpContext.Features.Get<ITrackingConsentFeature)، تامین مقدار Cancellation Token به کمک HttpContext.RequestAborted و یا حتی در صفحهی جدید Error.razor که آن نیز یک صفحهی SSR است، برای دریافت HttpContext?.TraceIdentifier استفادهی مستقیم شدهاست.
نکتهی مهم: باید بهخاطر داشت که فقط و فقط در صفحات SSR است که میتوان به HttpContext به نحو آبشاری فوق دسترسی یافت و همانطور که در قسمت قبل نیز بررسی شد، سایر حالات رندر Blazor از دسترسی به آن، پشتیبانی نمیکنند و اگر چنین پارامتری را تنظیم کردید، نال دریافت میکنید.
بررسی تفاوتهای تنظیمات ابتدایی قالب جدید Identity UI در Blazor 8x با نگارشهای قبلی آن
مطالب و نکات مرتبط با قالب قبلی را در مطلب «Blazor 5x - قسمت 22 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 2 - ورود به سیستم و خروج از آن» میتوانید مشاهده کنید.
در قسمت سوم این سری، روش ارتقاء یک برنامهی قدیمی Blazor Server را به نگارش 8x آن بررسی کردیم و یکی از تغییرات مهم آن، حذف فایلهای cshtml. ای آغاز برنامه و انتقال وظایف آن، به فایل جدید App.razor و انتقال مسیریاب Blazor به فایل جدید Routes.razor است که پیشتر در فایل App.razor نگارشهای قبلی Blazor وجود داشت.
در این نگارش جدید، محتوای فایل Routes.razor به همراه قالب Identity UI به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> </Router>
اما ... در اینجا با یک نگارش ساده شده سروکار داریم؛ از این جهت که برنامههای جدید، به همراه جزایر تعاملی هم میتوانند باشند و باید بتوان این AuthenticationState را به آنها هم ارسال کرد. به همین جهت مفهوم جدید مقادیر آبشاری سطح ریشه (Root-level cascading values) که پیشتر در این بحث معرفی شد، در اینجا برای معرفی AuthenticationState استفاده شدهاست.
در اینجا کامپوننت جدید FocusOnNavigate را هم مشاهده میکنید. با استفاده از این کامپوننت و براساس CSS Selector معرفی شده، پس از هدایت به یک صفحهی جدید، این المان مشخص شده دارای focus خواهد شد. هدف از آن، اطلاع رسانی به screen readerها در مورد هدایت به صفحهای دیگر است (برای مثال، کمک به نابیناها برای درک بهتر وضعیت جاری).
همچنین در اینجا المان NotFound را هم مشاهده نمیکنید. این المان فقط در برنامههای WASM جهت سازگاری با نگارشهای قبلی، هنوز هم قابل استفادهاست. جایگزین آنرا در قسمت سوم این سری، برای برنامههای Blazor server در بخش «ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده» آن بررسی کردیم.
روش انتقال اطلاعات سراسری اعتبارسنجی یک کاربر به کامپوننتها و جزایر تعاملی واقع در صفحات SSR
مشکل! زمانیکه از ترکیب صفحات SSR و جزایر تعاملی واقع در آن استفاده میکنیم ... جزایر واقع در آنها دیگر این مقادیر آبشاری را دریافت نمیکنند و این مقادیر در آنها نال است. برای حل این مشکل در Blazor 8x، باید مقادیر آبشاری سطح ریشه را (Root-level cascading values) به صورت زیر در فایل Program.cs برنامه ثبت کرد:
builder.Services.AddCascadingValue(sp =>new Preferences { Theme ="Dark" });
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
این AuthenticationStateProvider سفارشی جدید هم در فایل اختصاصی IdentityRevalidatingAuthenticationStateProvider.cs پوشهی Identity قالب پروژههای جدید Blazor 8x که به همراه اعتبارسنجی هستند، قابل مشاهدهاست.
یا اگر به قالبهای پروژههای WASM دار مراجعه کنید، تعریف به این صورت تغییر کردهاست؛ اما مفهوم آن یکی است:
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
AuthenticationStateProviderهای سفارشی مانند کلاسهای IdentityRevalidatingAuthenticationStateProvider و PersistingServerAuthenticationStateProvider که در این قالبها موجود هستند، چون به صورت آبشاری کار تامین AuthenticationState را انجام میدهند، به این ترتیب میتوان به شیء context.User.Identity@ در جزایر تعاملی نیز به سادگی دسترسی داشت.
@page "/auth" @using Microsoft.AspNetCore.Authorization @attribute [Authorize] <PageTitle>Auth</PageTitle> <h1>You are authenticated</h1> <AuthorizeView> Hello @context.User.Identity?.Name! </AuthorizeView>
سؤال: چگونه یک پروژهی سمت سرور، اطلاعات اعتبارسنجی خودش را با یک پروژهی WASM سمت کلاینت به اشتراک میگذارد (برای مثال در حالت رندر Auto)؟ این انتقال اطلاعات باتوجه به یکسان نبودن محل اجرای این دو پروژه (یکی بر روی کامپیوتر سرور و دیگری بر روی مرورگر کلاینت، در کامپیوتری دیگر) چگونه انجام میشود؟ این مورد را در قسمت بعد، با معرفی PersistentComponentState و «فیلدهای مخفی» مرتبط با آن، بررسی میکنیم.
using System.Collections.Generic;
using BLL = DotNetTipsBLLLayer;//نام مستعار برای فضای نام using EmployeeDomain = DotNetTipsBLLLayer.Employee;//نام مستعار برای یک نوع داده
using (var sqlConnection = new SqlConnection()) { //کد }
using static System.Console; using static System.Math; namespace dotnettipsUsingStatic { class Program { static void Main(string[] args) { Write(" *** Cal Area *** "); int r = int.Parse(ReadLine()); var result = Pow(r, 2) * PI; Write($"Area is : {result}"); ReadKey(); } } }
enum Gender { Male, Female }
تا قبل از سی شارپ 6 برای استفادهی از نوع داده شمارشی بدین شکل عمل میکردیم:
var gender = Gender.Male;
و اکنون بازنویسی استفادهی ازEnum به کمک ویژگی جدید static using statement :
در قسمت معرفی فضاهای نام بدین شکل عمل میکنیم:
using static dotnettipsUsingStatic.Gender;
و در برنامه کافیست مستقیما نام اعضای Enum را ذکر کنیم .
var gender = Male;//تخصیص نوع داده شمارشی WriteLine($"Employee Gender is : {Male}");//استفاده مستقیم از نوع داده شمارشی
استفاده از ویژگی using static و متدهای الحاقی :
تا قبل از ارائه سی شارپ 6 اگر نیاز به استفادهی از یک متد الحاقی خاص همچون where در فضای نام System.Linq.Enumeable داشتیم میبایستی فضای نام System.Linq را به طور کامل اضافه میکردیم و راهی برای اضافه کردن یک فضای نام خاص درون فضای نام بزرگتر وجود نداشت.
اما با قابلیت جدید اضافه شده میتوانیم بخشی از یک فضای نام را اضافه کنیم:
متدهای استاتیک و متدهای الحاقی در زمان استفاده از ویژگی using static:
فرض کنید کلاس static ای بنام MyStaticClass داشته باشیم که متد Print1 و Print2 در آن تعریف شده باشند:
public static class MyStaticClass { public static void Print1(string parameter) { WriteLine(parameter); } public static void Print2(this string parameter) { WriteLine(parameter); } }
برای استفاده از متدهای تعریف شده به شکل زیر عمل میکنیم :
//فراخوانی تابع استاتیک Print1("Print 1");//روش اول MyStaticClass.Print1("Prtint 1");//روش دوم //فراخوانی متد الحاقی استاتیک MyStaticClass.Print2("Print 2"); "print 2".Print2();
ویژگیهای جدید ارائه شده در سی شارپ 6 برای افزایش خوانایی برنامهها و تمیزتر شدن کدها اضافه شدهاند. در مورد ویژگیهای ارائه شده در مقالهی جاری این نکته مهم است که گاهی قید کردن نام کلاسها خود سبب افزایش خوانایی کدها میشود .
ASP.NET MVC #21
آشنایی با تکنیکهای Ajax در ASP.NET MVC
اهمیت آشنایی با Ajax، ارائه تجربه کاربری بهتری از برنامههای وب، به مصرف کنندگان نهایی آن میباشد. به این ترتیب میتوان درخواستهای غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواستها سبب میشود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین میتواند به سایر امور خود بپردازد. به این ترتیب میتوان برنامههای وبی را که شبیه به برنامههای دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمیشود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز میشود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر میباشد یا نه (نمونهای از آن را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).
مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو
با ایجاد هر پروژه ASP.NET MVC جدیدی در ویژوال استودیو، یک سری اسکریپت هم به صورت خودکار در پوشه Scripts آن اضافه میشوند. تعدادی از این فایلها توسط مایکروسافت پیاده سازی شدهاند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js
این فایلها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارشهای قبلی قرار دارند و استفاده از آنها اختیاری است. بنابراین با خیال راحت آنها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگیهای Ajax ایی، استفاده از کتابخانههای مرتبط با jQuery میباشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویسها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز میتوان استفاده کرد. همچنین حجم کمی نیز داشته، با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز میشود.
در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js
فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET میباشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده میشود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بینقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپتهای عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایلهای اسکریپتی که در این پوشه مشاهده میشوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.
به چهار نکته نیز حین استفاده از اسکریپتهای موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده میکنید، برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپتها در صفحه ذکر خواهد شد.
ب) ترتیب فایلهای js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونههای آنها.
ج) اگر اسکریپتهای jQuery در فایل layout سایت تعریف شدهاند؛ نیازی به تعریف مجدد آنها در Viewهای سایت نیست.
د) اگر View ایی به اسکریپت ویژهای جهت اجرا نیاز دارد، بهتر است آنرا به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آنرا در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).
یک نکته
اگر آخرین به روز رسانیهای ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه میشود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانیهای این کتابخانهها مطلع کند.
آشنایی با Ajax Helpers توکار ASP.NET MVC
اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:
using System;
namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}
علاوه بر خاصیت Html که وهلهای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم میکند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن میتوان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink میتوان قسمتی از صفحه را به کمک ویژگیهای Ajax، به روز رسانی کرد.
مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink
ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>
سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:
namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
using System.Collections.Generic;
namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}
در ادامه کنترلر جدیدی را به برنامه با محتوای زیر اضافه کنید:
using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;
namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");
if (!id.HasValue)
return View("Error");
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");
return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}
بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:
@model MvcApplication18.Models.Employee
<strong>Name:</strong> @Model.Name
سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>
<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>
توضیحات جزئیات کدهای فوق
متد Ajax.ActionLink لینکی را تولید میکند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگیهای jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص میگردد، بر اساس مقدار InsertionMode، درج خواهد شد (میتواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE مشکلی نباشد. از این جهت که IE پیغامهای GET را کش میکند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا میتوان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن میتوان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال میگردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج میگردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شدهاست که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آنرا خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب میتوان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظهای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسیها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
base.OnActionExecuting(filterContext);
}
else
{
throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
}
}
}
و برای استفاده از آن خواهیم داشت:
[AjaxOnly]
public ActionResult SomeAjaxAction()
{
return Content("Hello!");
}
در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نمودهاید. در اینجا نیز از ویژگیهای data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده میگردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم میشود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>
در کل این روش قابلیت نگهداری بهتری نسبت به روش اسکریپت نویسی مستقیم داخل صفحات را به همراه دارد. به علاوه جدا سازی افزونه اسکریپت وفق دهنده این اطلاعات با متد jQuery.Ajax از صفحه جاری، که امکان کش شدن آنرا به سادگی میسر میسازد.
به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink
الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز میتوان به نحو زیر انجام داد:
<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
توضیحات:
توسط متد jQuery.Ajax نیز میتوان درخواستهای Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص میکند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت میکند. البته حین استفاده از متد توکار Ajax.ActionLink، این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم میشود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص میکند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفتهایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی میشود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر میگرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپتهای مورد نیاز استفاده کردهایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:
@RenderSection("javascript", required: false)
دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax
اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه میشود؛ برای مثال میتوان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، میتوان از روشهایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:
[HttpPost] //for IE-8
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);
if (!id.HasValue)
return Json(false);
var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);
return Json(data);
}
سپس View برنامه را نیز به نحو زیر تغییر دهید:
<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}
در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس میتوان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.
ارسال اطلاعات فرمها به سرور، به کمک ویژگیهای Ajax
متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink میباشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:
[HttpPost] //for IE-8
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);
if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);
var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);
return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}
سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آنرا هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:
@model IEnumerable<MvcApplication18.Models.Employee>
<table>
<tr>
<th>
Name
</th>
</tr>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}
</table>
تا اینجا یک متد جستجو را ایجاد کردهایم که میتواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آنها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:
@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}
اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج میگردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده میشود.
متد Ajax.BeginForm نیز از ویژگیهای data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده میکند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.
<form action="/Home/SearchEmployeeInfo" data-ajax="true"
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>
کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونههای jQuery تدارک دیدهاند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/
افزودن فرمها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت
در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرمهای جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمیکند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی میگردد:
$.validator.unobtrusive.parse("#{form-id}");
اما با به روز رسانی قسمتی از صفحه، دیگر اینچنین نخواهد بود و نیاز است این فراخوانی را دستی انجام دهیم. برای مثال:
$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});
//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });
شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:
@using (Ajax.BeginForm(
"Action1",
"Controller",
null,
new AjaxOptions {
OnSuccess = "onSuccess",
UpdateTargetId = "result"
},
null)
)
{
<input type="submit" value="Save" />
}
var onSuccess = function(result) {
// enable unobtrusive validation for the contents
// that was injected into the <div id="result"></div> node
$.validator.unobtrusive.parse("#result");
};
در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر میشود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.