دریافت اطلاعات از سرور، توسط Axios
- ابتدا به پوشهی sample-22-backend ای که در قسمت قبل ایجاد کردیم، مراجعه کرده و فایل dotnet_run.bat آنرا اجرا کنید، تا endpointهای REST Api آن، قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts در مرورگر دسترسی یافت (و یا همانطور که عنوان شد، از آدرس https://jsonplaceholder.typicode.com/posts نیز میتوانید استفاده کنید؛ چون ساختار یکسانی دارند).
-سپس در برنامهی React ای که در قسمت قبل ایجاد کردیم، فایل app.js آنرا گشوده و ابتدا کتابخانهی Axios را import میکنیم:
import axios from "axios";
componentDidMount() { const promise = axios.get("https://localhost:5001/api/posts"); console.log(promise); }
تنظیمات CORS مخصوص React در برنامههای ASP.NET Core 3x
همانطور که مشاهده میکنید، پس از ذخیره سازی تغییرات، با اجرای برنامه، این Promise در حالت pending قرار گرفته و همچنین پس از پایان آن، حاوی نتیجهی عملیات نیز میباشد که در اینجا rejected است. علت شکست عملیات را در سطر بعدی آن ملاحظه میکنید که عنوان کردهاست «CORS policy» مناسبی در سمت سرور، برای این درخواست وجود ندارد؛ چرا؟ چون برنامهی React ما در مسیر http://localhost:3000/ اجرا میشود و برنامهی Web API در مسیر دیگری https://localhost:5001/ که شمارهی پورت ایندو یکی نیست. به همین جهت عنوان میکند که نیاز است در سمت سرور، هدرهای خاصی برای پردازش این نوع درخواستهای با Origin متفاوت وجود داشته باشد، تا مرورگر اجازهی دسترسی به آنرا بدهد. برای رفع این مشکل، برنامهی sample-22-backend را گشوده و تغییرات زیر را اعمال میکنیم:
ابتدا تنظیمات AddCors را با تعریف یک CORS policy جدید مخصوص آدرس http://localhost:3000، به متد ConfigureServices کلاس آغازین برنامه اضافه میکنیم:
public void ConfigureServices(IServiceCollection services) { services.AddCors(options => { options.AddPolicy("ReactCorsPolicy", builder => builder .AllowAnyMethod() .AllowAnyHeader() .WithOrigins("http://localhost:3000") .AllowCredentials() .Build()); }); services.AddSingleton<IPostsDataSource, PostsDataSource>(); services.AddControllers(); }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); //app.UseAuthentication(); //app.UseAuthorization(); app.UseCors("ReactCorsPolicy"); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
اینبار Promise بازگشت داده شده، در حالت resolved قرار گرفتهاست که به معنای موفقیت آمیز بودن عملیات async است. وجود [[PromiseStatus]] به معنای یک internal property است که توسط dot notation قابل دسترسی نیست. در اینجا [[PromiseValue]] نیز یک internal property غیرقابل دسترسی است که نتیجهی عملیات (response دریافتی از سرور) در آن قرار میگیرد. برای مثال در data آن، آرایهی مطالب دریافتی از سرور، قابل مشاهدهاست و یا status=200 به معنای موفقیت آمیز بودن پردازش درخواست، از سمت سرور است.
البته زمانیکه درخواست افزودن رکورد جدیدی را به سمت سرور ارسال میکنیم، میتوان دو درخواست را در برگهی network ابزارهای توسعه دهندگان مرورگر، مشاهده کرد:
در اولین درخواست، Request Method: OPTIONS را داریم که دقیقا مرتبط است با بررسی CORS توسط مرورگر.
دریافت اطلاعات شیء response از یک Promise و نمایش آن
همانطور که عنوان شد، [[PromiseValue]] نیز یک internal property غیرقابل دسترسی است. بنابراین اکنون این سؤال مطرح میشود که چگونه میتوان به اطلاعات آن دسترسی یافت؟
این شیء Promise، دارای متدی است به نام then است که نتیجهی عملیات async را بازگشت میدهد. البته این روش قدیمی کار کردن با Promiseها است و ما از آن در اینجا استفاده نخواهیم کرد. در جاوا اسکریپت مدرن، میتوان از واژهی کلیدی await برای دسترسی به شیء response دریافتی از سرور، استفاده کرد:
async componentDidMount() { const promise = axios.get("https://localhost:5001/api/posts"); console.log(promise); const response = await promise; console.log(response); }
البته قطعه کد نوشته شده، صرفا جهت توضیح مراحل مختلف عملیات، به این صورت چند مرحلهای نوشته شد، وگرنه میتوان واژهی کلیدی await را پیش از فراخوانی متدهای Axios نیز قرار داد:
async componentDidMount() { const response = await axios.get("https://localhost:5001/api/posts"); console.log(response); }
class App extends Component { state = { posts: [] }; async componentDidMount() { const { data: posts } = await axios.get("https://localhost:5001/api/posts"); this.setState({ posts }); // = { posts: posts } }
ایجاد یک مطلب جدید توسط Axios
در برنامهی React ای ایجاد شده، یک دکمهی Add نیز برای افزودن مطلبی جدید درنظر گرفته شدهاست. در یک برنامهی واقعیتر، معمولا فرمی وجود دارد و نتیجهی آن در حین submit، به سمت سرور ارسال میشود. در اینجا این سناریو را شبیه سازی خواهیم کرد:
const apiEndpoint = "https://localhost:5001/api/posts"; class App extends Component { state = { posts: [] }; async componentDidMount() { const { data: posts } = await axios.get(apiEndpoint); this.setState({ posts }); } handleAdd = async () => { const newPost = { title: "new Title ...", body: "new Body ...", userId: 1 }; const { data: post } = await axios.post(apiEndpoint, newPost); console.log(post); const posts = [post, ...this.state.posts]; this.setState({ posts }); };
- چون قرار است از آدرس https://localhost:5001/api/posts در قسمتهای مختلف برنامه استفاده کنیم، فعلا آنرا به صورت یک ثابت تعریف کرده و در متدهای get و post استفاده کردیم.
- در متد منتسب به خاصیت handleAdd، یک شیء جدید post را با ساختاری مشابه آن ایجاد کردهایم. این شیء جدید، دارای Id نیست؛ چون قرار است از سمت سرور پس از ثبت در بانک اطلاعاتی دریافت شود.
- سپس این شیء جدید را توسط متد post کتابخانهی Axios، به سمت سرور ارسال کردهایم. این متد نیز یک Promise را باز میگرداند. به همین جهت از واژهی کلیدی await برای دریافت نتیجهی واقعی آن استفاده شدهاست. همچنین هر زمانیکه await داریم، نیاز به ذکر واژهی کلیدی async نیز هست. اینبار این واژه باید پیش از قسمت تعریف پارامتر متد قرار گیرد و نه پیش از نام handleAdd؛ چون handleAdd در واقع یک خاصیت است که متدی به آن انتساب داده شدهاست.
- نتیجهی دریافتی از متد axios.post را اینبار به post، بجای posts تغییر نام دادهایم و همانطور که در تصویر زیر مشاهده میکنید، خاصیت id آن در سمت سرور مقدار دهی شدهاست:
- در آخر برای افزودن این رکورد، به مجموعهی رکوردهای موجود، از روش spread operator استفاده کردهایم تا ابتدا شیء post دریافتی از سمت سرور درج شود و سپس مابقی اعضای آرایهی posts موجود در state، در این آرایه گسترده شده و یک آرایهی جدید را تشکیل دهند. سپس این آرایهی جدید را جهت به روز رسانی state و در نتیجهی آن، به روز رسانی UI، به متد setState ارسال کردهایم، که نتیجهی آن درج این رکورد جدید، در ابتدای لیست است:
به روز رسانی اطلاعات در سمت سرور
در اینجا پیاده سازی متد put را مشاهده میکنید:
handleUpdate = async post => { post.title = "Updated"; const { data: updatedPost } = await axios.put( `${apiEndpoint}/${post.id}`, post ); console.log(updatedPost); const posts = [...this.state.posts]; const index = posts.indexOf(post); posts[index] = { ...post }; this.setState({ posts }); };
- اکنون امضای متد axios.put هرچند مانند متد post است، اما متد Update تعریف شدهی در سمت API سرور، یک چنین مسیری را نیاز دارد api/Posts/{id}. به همین جهت ذکر id مطلب، در URL نهایی نیز ضروری است.
- در اینجا نیز از واژههای await و async برای دریافت نتیجهی واقعی عملیات put و همچنین عملیات گذاری این متد به صورت async، استفاده شدهاست.
- در آخر، ابتدا آرایهی posts موجود در state را clone میکنیم. چون میخواهیم در آن، در ایندکسی که شیء post جاری قرار دارد، مقدار به روز رسانی شدهی آنرا قرار دهیم. سپس این آرایهی جدید را جهت به روز رسانی state و در نتیجهی آن، به روز رسانی UI، به متد setState ارسال کردهایم:
حذف اطلاعات در سمت سرور
برای حذف اطلاعات در سمت سرور، نیاز است یک HTTP Delete را به آن ارسال کنیم که اینکار را میتوان توسط متد axios.delete انجام داد. URL ای را که دریافت میکند، شبیه به URL ای است که برای حالت put ایجاد کردیم:
handleDelete = async post => { await axios.delete(`${apiEndpoint}/${post.id}`); const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); };
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-backend-part-02.zip و sample-22-frontend-part-02.zip
آشنایی با Gridify
[HttpGet("[action]")] public async Task<IActionResult> UsersList() { var users = await _dbContext.Users.AsNoTracking().ToListAsync(); return Ok(users); }
- عدم پشتیبانی از Pagination: چون API، تمامی کاربران را به سمت کلاینت ارسال میکند؛ به همین جهت، هم با مشکل کارآیی (performance) در آینده مواجه میشویم و هم امکان گذاشتن صفحه بندی (Pagination) وجود نخواهد داشت.
- عدم پشتیبانی از Sorting: اگر در گرید نمایش داده شده کاربر بخواهد اطلاعات را Sort کند، چون چنین امکانی هنوز برای API ما تعریف نشده، این عملیات سمت سرور امکان پذیر نیست.
- عدم پشتیبانی از Filtering: همیشه نمایش تمامی اطلاعات مفید نیست. در اکثر مواقع ما نیاز داریم تا قسمتی از اطلاعات را با شرطی خاص، برگردانیم. به طور مثال لیست کاربران فعال در سامانه یا لیست کاربران غیرفعال.
[HttpGet("[action]")] public async Task<IActionResult> UsersList(GridifyQuery filter) { var users = await _dbContext.Users.AsNoTracking().GridifyAsync(filter); return Ok(users); }
var result = Users.AsQueryable().ApplyFiltering("name==Ali");
var result = Users.AsQueryable().Where(q => q.Name == "Ali");
الگوی معکوس سازی کنترل چیست؟
IoC یک الگوی سطح بالا است و به روشهای مختلفی به مسایل متفاوتی جهت معکوس سازی کنترل، قابل اعمال میباشد؛ مانند:
- کنترل اینترفیسهای بین دو سیستم
- کنترل جریان کاری برنامه
- کنترل بر روی ایجاد وابستگیها (جایی که تزریق وابستگیها و DI ظاهر میشوند)
سؤال: بین IoC و DIP چه تفاوتی وجود دارد؟
در DIP (قسمت قبل) به این نتیجه رسیدیم که یک ماژول سطح بالاتر نباید به جزئیات پیاده سازیهای ماژولی سطح پایینتر وابسته باشد. هر دوی اینها باید بر اساس Abstraction با یکدیگر ارتباط برقرار کنند. IoC روشی است که این Abstraction را فراهم میکند. در DIP فقط نگران این هستیم که ماژولهای موجود در لایههای مختلف برنامه به یکدیگر وابسته نباشند اما بیان نکردیم که چگونه.
معکوس سازی اینترفیسها
هدف از معکوس سازی اینترفیسها، استفاده صحیح و معنا دار از اینترفیسها میباشد. به این معنا که صرفا تعریف اینترفیسها به این معنا نیست که طراحی صحیحی در برنامه بکار گرفته شده است و در حالت کلی هیچ معنای خاصی ندارد و ارزشی را به برنامه و سیستم شما اضافه نخواهد کرد.
برای مثال یک مسابقه بوکس را درنظر بگیرید. در اینجا Ali یک بوکسور است. مطابق عادت معمول، یک اینترفیس را مخصوص این کلاس ایجاد کرده، به نام IAli و مسابقه بوکس از آن استفاده خواهد کرد. در اینجا تعریف یک اینترفیس برای Ali، هیچ ارزش افزودهای را به همراه ندارد و متاسفانه عادتی است که در بین بسیاری از برنامه نویسها متداول شده است؛ بدون اینکه علت واقعی آنرا بدانند و تسلطی به الگوهای طراحی برنامه نویسی شیءگرا داشته باشند. صرف اینکه به آنها گفته شده است تعریف اینترفیس خوب است، سعی میکنند برای همه چیز اینترفیس تعریف کنند!
تعریف یک اینترفیس تنها زمانی ارزش خواهد داشت که چندین پیاده سازی از آن ارائه شود. در مثال ما پیاده سازیهای مختلفی از اینترفیس IAli بیمفهوم است. همچنین در دنیای واقعی، در یک مسابقه بوکس، چندین و چند شرکت کننده وجود خواهند داشت. آیا باید به ازای هر کدام یک اینترفیس جداگانه تعریف کرد؟ ضمنا ممکن است اینترفیس IAli متدی داشته باشد به نام ضربه، اینترفیس IVahid متد دیگری داشته باشد به نام دفاع.
کاری که در اینجا جهت طراحی صحیح باید صورت گیرد، معکوس سازی اینترفیسها است. به این ترتیب که مسابقه بوکس است که باید اینترفیس مورد نیاز خود را تعریف کند و آن هم تنها یک اینترفیس است به نام IBoxer. اکنون Ali، Vahid و سایرین باید این اینترفیس را جهت شرکت در مسابقه بوکس پیاده سازی کنند. بنابراین دیگر صرف وجود یک کلاس، اینترفیس مجزایی برای آن تعریف نشده و بر اساس معکوس سازی کنترل است که تعریف اینترفیس IBoxer معنا پیدا کرده است. اکنون IBoxer دارای چندین و چند پیاده سازی خواهد بود. به این ترتیب، تعریف اینترفیس، ارزشی را به سیستم افزوده است.
به این نوع معکوس سازی اینترفیسها، الگوی provider model نیز گفته میشود. برای مثال کلاسی که از چندین سرویس استفاده میکند، بهتر است یک IService را ایجاد کرده و تامین کنندههایی، این IService را پیاده سازی کنند. نمونهای از آن در دنیای دات نت، Membership Provider موجود در ASP.NET است که پیاده سازیهای بسیاری از آن تاکنون تهیه و ارائه شدهاند.
معکوس سازی جریان کاری برنامه
جریان کاری معمول یک برنامه یا Noraml flow، عموما رویهای یا Procedural است؛ به این معنا که از یک مرحله به مرحلهای بعد هدایت خواهد شد. برای مثال یک برنامه خط فرمان را درنظر بگیرید که ابتدا میپرسد نام شما چیست؟ در مرحله بعد مثلا رنگ مورد علاقه شما را خواهد پرسید.
برای معکوس سازی این جریان کاری، از یک رابط کاربری گرافیکی یا GUI استفاده میشود. مثلا یک فرم را درنظر بگیرید که در آن دو جعبه متنی، کار دریافت نام و رنگ را به عهده دارند؛ به همراه یک دکمه ثبت اطلاعات. به این ترتیب بجای اینکه برنامه، مرحله به مرحله کاربر را جهت ثبت اطلاعات هدایت کند، کنترل به کاربر منتقل و معکوس شده است.
معکوس سازی تولید اشیاء
معکوس سازی تولید اشیاء، اصل بحث دوره و سری جاری را تشکیل میدهد و در ادامه مباحث، بیشتر و عمیقتر بررسی خواهد گردید.
روش متداول تعریف و استفاده از اشیاء دیگر درون یک کلاس، وهله سازی آنها توسط کلمه کلیدی new است. به این ترتیب از یک وابستگی به صورت مستقیم درون کدهای کلاس استفاده خواهد شد. بنابراین در این حالت کلاسهای سطح بالاتر به ماژولهای سطح پایین، به صورت مستقیم وابسته میگردند.
برای اینکه این کنترل را معکوس کنیم، نیاز است ایجاد و وهله سازی این اشیاء وابستگی را در خارج از کلاس جاری انجام دهیم. شاید در اینجا بپرسید که چرا؟
اگر با الگوی طراحی شیءگرای Factory آشنا باشید، همان ایده در اینجا مدنظر است:
Button button; switch (UserSettings.UserSkinType) { case UserSkinTypes.Normal: button = new Button(); break; case UserSkinTypes.Fancy: button = new FancyButton(); break; }
حال در این برنامه اگر قرار باشد کار و کنترل محل وهله سازی این دکمهها معکوس نشود، در هر قسمتی از برنامه نیاز است این سوئیچ تکرار گردد (برای مثال در چند ده فرم مختلف برنامه). بنابراین بهتر است محل ایجاد این دکمهها به کلاس دیگری منتقل شود مانند ButtonFactory و سپس از این کلاس در مکانهای مختلف برنامه استفاده گردد:
Button button = ButtonFactory.CreateButton();
بنابراین در مثال فوق، کنترل ایجاد دکمهها به یک کلاس پایه قرار گرفته در خارج از کلاس جاری، معکوس شده است.
انواع معکوس سازی تولید اشیاء
بسیاری شاید تصور کنند که تنها راه معکوس سازی تولید اشیاء، تزریق وابستگیها است؛ اما روشهای چندی برای انجام اینکار وجود دارد:
الف) استفاده از الگوی طراحی Factory (که نمونهای از آنرا در قسمت قبل مشاهده کردید)
ب) استفاده از الگوی Service Locator
Button button = ServiceLocator.Create(IButton.Class)
ج) تزریق وابستگیها
Button button = GetTheButton(); Form1 frm = new Form1(button);
به صورت خلاصه هر زمانیکه تولید و وهله سازی وابستگیهای یک کلاس را به خارج از آن منتقل کردید، کار معکوس سازی تولید وابستگیها انجام شده است.
استفاده از کتابخانههای جاوا اسکریپتی ثالث
برای استفاده از کتابخانههای جاوا اسکریپتی ثالث، نیاز است آنها را به فایل angular-cli.json. معرفی کنیم:
"apps": [ { "assets": [ "assets", "favicon.ico" ], "styles": [ "styles.css" ], "scripts": [],
به علاوه تعریف پوشهی src\assets را نیز در اینجا مشاهده میکنید؛ به همراه فایلهای اضافی دیگری مانند src\favicon.ico که ذیل آن ذکر شدهاست.
یک مثال: معرفی کتابخانهی ng2-bootstrap به Angular CLI
دریافت و نصب بستههای مورد نیاز
مرحلهی اول کار با یک کتابخانهی ثالث نوشته شدهی برای Angular مانند ngx-bootstrap، دریافت و نصب بستهی npm آن میباشد. به همین جهت به ریشهی پروژه وارد شده و دستورات ذیل را صادر کنید تا بوت استرپ و همچنین کامپوننتهای +Angular 2.0 آن نصب شوند:
> npm install bootstrap --save > npm install ngx-bootstrap --save
پرچم save در اینجا سبب به روز رسانی خودکار فایل package.json میشود:
"dependencies": { "bootstrap": "^3.3.7", "ngx-bootstrap": "^1.6.6",
معرفی بستههای نصب شده به تنظیمات Angular CLI
پس از آن، همانطور که عنوان شد نیاز است به فایل angular-cli.json. مراجعه کرده و شیوهنامهی بوت استرپ را تعریف کنیم:
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
چون از ngx-bootstrap استفاده میکنیم، نیازی به مقدار دهی مستقیم []:"scripts" فایل angular-cli.json. نیست. ولی اگر خواستید اینکار را انجام دهید، روش آن به صورت ذیل است (که البته نیاز به نصب بستهی jQuery را نیز خواهد داشت):
"scripts": [ "../node_modules/jquery/dist/jquery.js", "../node_modules/bootstrap/dist/js/bootstrap.js" ],
بنابراین تا اینجا بستههای بوت استرپ و همچنین ngx-bootstarp نصب شدند و شیوهنامهی بوت استرپ به فایل angular-cli.json اضافه گردید (نیازی هم به تکمیل قسمت scripts نیست).
استفاده از ماژولهای مختلف بستهی نصب شده در برنامه
در ادامه نیاز است تا ماژولی را از ngx-bootstarp را به قسمت imports فایل src\app\app.component.ts اضافه کرد. هرکدام از کامپوننتهای این بسته به صورت یک ماژول مجزا تعریف شدهاند. بنابراین برای استفادهی از آنها نیاز است برنامه را از وجودشان مطلع کرد. برای مثال روش استفادهی از AlertModule آن به صورت ذیل است:
import { AlertModule } from 'ngx-bootstrap'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, FormsModule, HttpModule, AlertModule.forRoot() ], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
آزمایش برنامه و اجرای آن
برای آزمایش مراحل فوق، فایل src/app/app.component.html را گشوده و به صورت ذیل تغییر دهید:
<h1> {{title}} </h1> <button class="btn btn-primary">Hello!</button> <alert type="success">Alert success!</alert>
اکنون اگر دستور ng serve -o را اجرا کنیم، خروجی ذیل حاصل خواهد شد:
مستندات و مثالهای بیشتری را از ماژولهای ngx-bootstarp، در اینجا میتوانید بررسی کنید.
برای این کار اولین چیزی که لازم بود دریافت و ذخیره اطلاعات بود که من برای این کار از Entity framework 4.1 Database-first و کتابخانه htmlagilitypack - HAP استفاده کردم . طراحی دیتابیس نهایی به این صورت شد
خوب در تلاش اول و مبتدیانه و بدون استفاده از این کتابخانه مفید چون اکثر صفحات وب XHTML نیستند و بالاخره چند تگ درست بسته نشده دارند و شما اگر بخواهید در آبجکت XmlDocument این htmlهای به ظاهر سالم رو لود کنید فورا با استثنای زیر مواجه میشوید
XmlException Was unhandeled The 'img' start tag on line 1 position 1604 does not match the end tag of 'a'. Line 1, position 1766
PM> Install-Package HtmlAgilityPack
مثلا با کد زیر میشه تاریخ تولد یک ورزشکار رو بدست آورد .توابع دیگه ای که خیلی جاها میتونه بدرد خورد GetAttributeValue و ChildNodes هست که یک نمونه نحوه استفادشو در ادامه میبینید
HtmlDocument xhtml = Crawler.GetXHtmlFromUri("http://www.london2012.com/athlete/hadadi-ehsan-1077408/"); HtmlNode tempNode = xhtml.DocumentNode.SelectSingleNode("//table[@class='athleteBio']/tbody/tr[4]");
string temp = tempNode.FirstChild.FirstChild.InnerText.Replace(" ", "").Trim(); athlete.Birthday = DateTime.Parse(temp.Substring(0, 10), new CultureInfo("en-GB"));
tempNode = xhtml.DocumentNode.SelectSingleNode("//div[@class='athletePhotoMedals']/div/div/img"); athlete.LargePhotoUri = tempNode.GetAttributeValue("src", "");
نکته اصلی هم پیدا کردن محل دقیق اطلاعاته که با ابزاری مثل Firebug خیلی راحتتر میشه این کارو انجام داد. کافیه روی تاریخ تولد راست کلیک و inspect element by Firebug رو بزنید و حالا اگر تویه dom روی هر المنت html نگه دارید بهتون XPath کامل رو میده که میتونید تویه تابع DocumentNode.SelectSingleNode ازش استفاده کنید.
برای درک بهتر XPath هم این 2 تا صفحه xpath_syntax و xpath_examples خیلی میتونه کمکتون بکنه.
یک captcha حرفهای
بدین منظور فریم ورک ASP.NET Web API کتابخانه ای برای تولید خودکار صفحات راهنما در زمان اجرا (run-time) فراهم کرده است.
ایجاد صفحات راهنمای API
برای شروع ابتدا ابزار ASP.NET and Web Tools 2012.2 Update را نصب کنید. اگر از ویژوال استودیو 2013 استفاده میکنید این ابزار بصورت خودکار نصب شده است. این ابزار صفحات راهنما را به قالب پروژههای ASP.NET Web API اضافه میکند.
یک پروژه جدید از نوع ASP.NET MVC Application بسازید و قالب Web API را برای آن انتخاب کنید. این قالب پروژه کنترلری بنام ValuesController را بصورت خودکار برای شما ایجاد میکند. همچنین صفحات راهنمای API هم برای شما ساخته میشوند. تمام کد مربوط به صفحات راهنما در قسمت Areas قرار دارند.
اگر اپلیکیشن را اجرا کنید خواهید دید که صفحه اصلی لینکی به صفحه راهنمای API دارد. از صفحه اصلی، مسیر تقریبی Help/ خواهد بود.
این لینک شما را به یک صفحه خلاصه (summary) هدایت میکند.
نمای این صفحه در مسیر Areas/HelpPage/Views/Help/Index.cshtml قرار دارد. میتوانید این نما را ویرایش کنید و مثلا قالب، عنوان، استایلها و دیگر موارد را تغییر دهید.
بخش اصلی این صفحه متشکل از جدولی است که APIها را بر اساس کنترلر طبقه بندی میکند. مقادیر این جدول بصورت خودکار و توسط اینترفیس IApiExplorer تولید میشوند. در ادامه مقاله بیشتر درباره این اینترفیس صحبت خواهیم کرد. اگر کنترلر جدیدی به API خود اضافه کنید، این جدول بصورت خودکار در زمان اجرا بروز رسانی خواهد شد.
ستون "API" متد HTTP و آدرس نسبی را لیست میکند. ستون "Documentation" مستندات هر API را نمایش میدهد. مقادیر این ستون در ابتدا تنها placeholder-text است. در ادامه مقاله خواهید دید چگونه میتوان از توضیحات XML برای تولید مستندات استفاده کرد.
هر API لینکی به یک صفحه جزئیات دارد، که در آن اطلاعات بیشتری درباره آن قابل مشاهده است. معمولا مثالی از بدنههای درخواست و پاسخ هم ارائه میشود.
افزودن صفحات راهنما به پروژه ای قدیمی
می توانید با استفاده از NuGet Package Manager صفحات راهنمای خود را به پروژههای قدیمی هم اضافه کنید. این گزینه مخصوصا هنگامی مفید است که با پروژه ای کار میکنید که قالب آن Web API نیست.
از منوی Tools گزینههای Library Package Manager, Package Manager Console را انتخاب کنید. در پنجره Package Manager Console فرمان زیر را وارد کنید.
Install-Package Microsoft.AspNet.WebApi.HelpPage
@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)
همانطور که مشاهده میکنید مسیر نسبی صفحات راهنما "Help/" میباشد. همچنین اطمینان حاصل کنید که ناحیهها (Areas) بدرستی رجیستر میشوند. فایل Global.asax را باز کنید و کد زیر را در صورتی که وجود ندارد اضافه کنید.
protected void Application_Start() { // Add this code, if not present. AreaRegistration.RegisterAllAreas(); // ... }
افزودن مستندات API
بصورت پیش فرض صفحات راهنما از placeholder-text برای مستندات استفاده میکنند. میتوانید برای ساختن مستندات از توضیحات XML استفاده کنید. برای فعال سازی این قابلیت فایل Areas/HelpPage/App_Start/HelpPageConfig.cs را باز کنید و خط زیر را از حالت کامنت درآورید:
config.SetDocumentationProvider(new XmlDocumentationProvider( HttpContext.Current.Server.MapPath("~/App_Data/XmlDocument.xml")));
زیر قسمت Output گزینه XML documentation file را تیک بزنید و در فیلد روبروی آن مقدار "App_Data/XmlDocument.xml" را وارد کنید.
حال کنترلر ValuesController را از مسیر Controllers/ValuesController.cs/ باز کنید و یک سری توضیحات XML به متدهای آن اضافه کنید. بعنوان مثال:
/// <summary> /// Gets some very important data from the server. /// </summary> public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } /// <summary> /// Looks up some data by ID. /// </summary> /// <param name="id">The ID of the data.</param> public string Get(int id) { return "value"; }
اپلیکیشن را مجددا اجرا کنید و به صفحات راهنما بروید. حالا مستندات API شما باید تولید شده و نمایش داده شوند.
صفحات راهنما مستندات شما را در زمان اجرا از توضیحات XML استخراج میکنند. دقت کنید که هنگام توزیع اپلیکیشن، فایل XML را هم منتشر کنید.
توضیحات تکمیلی
صفحات راهنما توسط کلاس ApiExplorer تولید میشوند، که جزئی از فریم ورک ASP.NET Web API است. به ازای هر API این کلاس یک ApiDescription دارد که توضیحات لازم را در بر میگیرد. در اینجا منظور از "API" ترکیبی از متدهای HTTP و مسیرهای نسبی است. بعنوان مثال لیست زیر تعدادی API را نمایش میدهد:
- GET /api/products
- {GET /api/products/{id
- POST /api/products
اگر اکشنهای کنترلر از متدهای متعددی پشتیبانی کنند، ApiExplorer هر متد را بعنوان یک API مجزا در نظر خواهد گرفت. برای مخفی کردن یک API از ApiExplorer کافی است خاصیت ApiExplorerSettings را به اکشن مورد نظر اضافه کنید و مقدار خاصیت IgnoreApi آن را به true تنظیم نمایید.
[ApiExplorerSettings(IgnoreApi=true)] public HttpResponseMessage Get(int id) { }
همچنین میتوانید این خاصیت را به کنترلرها اضافه کنید تا تمام کنترلر از ApiExplorer مخفی شود.
کلاس ApiExplorer متن مستندات را توسط اینترفیس IDocumentationProvider دریافت میکند. کد مربوطه در مسیر Areas/HelpPage/XmlDocumentation.cs/ قرار دارد. همانطور که گفته شد مقادیر مورد نظر از توضیحات XML استخراج میشوند. نکته جالب آنکه میتوانید با پیاده سازی این اینترفیس مستندات خود را از منبع دیگری استخراج کنید. برای اینکار باید متد الحاقی SetDocumentationProvider را هم فراخوانی کنید، که در HelpPageConfigurationExtensions تعریف شده است.
کلاس ApiExplorer بصورت خودکار اینترفیس IDocumentationProvider را فراخوانی میکند تا مستندات APIها را دریافت کند. سپس مقادیر دریافت شده را در خاصیت Documentation ذخیره میکند. این خاصیت روی آبجکتهای ApiDescription و ApiParameterDescription تعریف شده است.
مطالعه بیشتر
پروژه بنده به صورتی هست که در آن قرار است یک سری اطلاعات توسط DataTable به Report ارسال و ریپورت هم Fieldها را که از قبل طراحی شده، روی فرم داشته باشد و کاربر فقط این امکان را داشته باشد که مکان فیلدها ( یعنی مختصات ) روی صفحه A4 را با توجه به سلیقه خودش تنظیم و ذخیره نماید.
اهداف :
1.غیر فعال کردن بعضی از امکانات صفحه Design
2.اعمال یکسری تنظیمات از طریق کدنویسی (Code Behind) بر روی ریپورت
مانند : اینکه ShowGrid فعال باشه یا نه -- یا Toolbox.visible=false باشه
3.فارسی سازی محیط طراحی برای کاربر
4.ذخیره و ...
اسکرین شات : ( حالت پیشفرض بدون اعمال تغییرات Runtime )
رفرنسهای مورد نیاز:
using Stimulsoft.Base.Services; using Stimulsoft.Report; using Stimulsoft.Report.Components; using Stimulsoft.Report.Design; using Stimulsoft.Report.Design.Toolbars; using Stimulsoft.Report.Render; using Stimulsoft.Report.Units;
//ایجاد یک شی از ریپورت StiReport report = new StiReport(); //ریست کردن تنظیمات به حالت پیشفرض report.Reset(); //ریست کردن تنظیمات سرویسهای StiConfig.Reset(); //ریست کردن تنظیمات چاپ StiSettings.Clear(); //تنظیم عنوان ریپورت به صورت دلخواه StiOptions.Designer.DesignerTitle = title + " طراحی فرم "; StiOptions.Designer.DesignerTitleText = title + " طراحی فرم "; //غیرفعال شدن نمایش تب کدنویسی StiOptions.Designer.CodeTabVisible = false; //فعال کردن امکان RightToLeft StiOptions.Designer.UseRightToLeftGlobalizationEditor = true; //غیرفعال شدن قابلیت تغییر نام ریپورت توسط کاربر StiOptions.Designer.CanDesignerChangeReportFileName = false; //غیر فعال کردن تشخیص اتوماتیک زبان پیش فرض UI طراحی StiOptions.Designer.UseSimpleGlobalizationEditor = false; //تنظیم تم ریپورت از حالت استاندارد به ریبون StiOptions.Windows.GlobalGuiStyle = StiGlobalGuiStyle.Office2010Blue; //فعال سازی تم ریبون StiOptions.Designer.IsRibbonGuiEnabled = true;
یک شی از StiOptions ایجاد کنید. سپس Designer و فعال و غیرفعال کردن نمایش هر بخش را و حتی تغییر آیتمهای موجود در منوی راست کلیک هر شی، قابل تغییر خواهند بود.
پنل Dictionary از سه شاخه اصلی تشکیل میشود : که با دستورات زیر میتوان نمایش این بخشها را در صورت خالی بودن از داده غیر فعال نمود
BusinessObjectsCategory - DataSourcesCategory -VariablesCategory
StiOptions.Designer.Panels.Dictionary.ShowEmptyBusinessObjectsCategory = false; StiOptions.Designer.Panels.Dictionary.ShowEmptyDataSourcesCategory = false; StiOptions.Designer.Panels.Dictionary.ShowEmptyVariablesCategory = false;
1- Dictionary
2- Properties
3- Report Tree
که Properties نسبت به هر شیء ایی که از صفحه ریپورت انتخاب میکنید، تنظیمات مربوط به آن را برای ویرایش در اختیار کاربر قرار میدهد.
پنل Report Tree نیز از دادههای موجود از دیتاسورس بخش Dictionary که به صورت شیء در صفحه ریپورت قرار داده شدهاند نمایش درخت وارهای را در اختیار کاربر قرار میدهد و میتواند از اشیاء این درخت واره در ریپورت به صورت متعدد استفاده نماید.
میتوان هر کدام از این پنلها را ( که به صورت سرویس در Stimulsoft تعریف شده) از دید کاربر مخفی نمود یا به صورت محدود یکسری از قابلیتها را در اختیار کاربر قرار داد:
//غیر فعال کردن سرویسهای پنل Stimulsoft.Report.Design.Panels.StiPropertiesPanelService propPanel = Stimulsoft.Report.Design.Panels.StiPropertiesPanelService.GetService(); propPanel.ServiceEnabled = false; Stimulsoft.Report.Design.Panels.StiDictionaryPanelService dictPanel = Stimulsoft.Report.Design.Panels.StiDictionaryPanelService.GetService(); dictPanel.ServiceEnabled = true; Stimulsoft.Report.Design.Panels.StiReportTreePanelService treePanel = Stimulsoft.Report.Design.Panels.StiReportTreePanelService.GetService(); treePanel.ServiceEnabled = false; Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService cpanel = Stimulsoft.Report.Design.Toolbars.StiToolsToolbarService.GetService(); cpanel.ServiceEnabled = false; StiOptions.Dictionary.BusinessObjects.AddBusinessObjectAssemblyToReferencedAssembliesAutomatically = false; StiOptions.Dictionary.BusinessObjects.AllowProcessNullItemsInEnumerables = false; StiOptions.Dictionary.BusinessObjects.AllowUseDataColumn = false; StiOptions.Dictionary.BusinessObjects.AllowUseFields = false; StiOptions.Dictionary.BusinessObjects.AllowUseProperties = false; StiOptions.Dictionary.BusinessObjects.CheckTableDuplication = false; StiOptions.Dictionary.ShowOnlyAliasForDataSource = true; StiOptions.Dictionary.ShowOnlyAliasForDataColumn = true; StiOptions.Dictionary.ShowOnlyAliasForTotal = true; dictPanel.ShowNewButton = false; dictPanel.ShowActionsButton = false; dictPanel.ShowBusinessObjectNewMenuItem = false; dictPanel.ShowCalcColumnNewMenuItem = false; dictPanel.ShowCategoryNewMenuItem = false; dictPanel.ShowCollapseAllMenuItem = true; dictPanel.ShowColumnNewMenuItem = false; dictPanel.ShowConnectionNewMenuItem = false; dictPanel.ShowContextMenu = false; dictPanel.ShowCreateFieldOnDoubleClick = false; dictPanel.ShowCreateLabel = false; dictPanel.ShowDataParameterNewMenuItem = false; dictPanel.ShowDataSourceNewMenuItem = false; dictPanel.ShowDataSourcesNewMenuItem = false; dictPanel.ShowDeleteButton = false; dictPanel.ShowDeleteForBusinessObject = false; dictPanel.ShowDeleteForDataColumn = false; dictPanel.ShowDeleteForDataConnection = false; dictPanel.ShowDeleteForDataParameter = false; dictPanel.ShowDeleteForDataRelation = false; dictPanel.ShowDeleteForDataSource = false; dictPanel.ShowDeleteForVariable = false; dictPanel.ShowDeleteMenuItem = false; dictPanel.ShowDictMergeMenuItem = false; dictPanel.ShowDictNewMenuItem = false; dictPanel.ShowDictOpenMenuItem = false; dictPanel.ShowDictSaveMenuItem = false; dictPanel.ShowDictXmlExportMenuItem = false; dictPanel.ShowDictXmlImportMenuItem = false; dictPanel.ShowDictXmlMergeMenuItem = false; dictPanel.ShowDownButton = false; dictPanel.ShowEditButton = false; dictPanel.ShowEditForBusinessObject = false; dictPanel.ShowEditForDataColumn = false; dictPanel.ShowEditForDataConnection = false; dictPanel.ShowEditForDataParameter = false; dictPanel.ShowEditForDataRelation = false; dictPanel.ShowEditForDataSource = false; dictPanel.ShowEditForVariable = false; dictPanel.ShowEditMenuItem = false; dictPanel.ShowExpandAllMenuItem = true; dictPanel.ShowMarkUsedMenuItem = false; dictPanel.ShowNewButton = false; dictPanel.ShowPropertiesForBusinessObject = false; dictPanel.ShowPropertiesForDataColumn = false; dictPanel.ShowPropertiesForDataConnection = false; dictPanel.ShowPropertiesForDataParameter = false; dictPanel.ShowPropertiesForDataRelation = false; dictPanel.ShowPropertiesForDataSource = false; dictPanel.ShowPropertiesForVariable = false; dictPanel.ShowPropertiesMenuItem = false; dictPanel.ShowRelationNewMenuItem = false; dictPanel.ShowRelationsImportMenuItem = false; dictPanel.ShowRemoveUnusedMenuItem = false; dictPanel.ShowSortItemsButton = false; dictPanel.ShowSynchronizeMenuItem = false; dictPanel.ShowUpButton = false; dictPanel.ShowUseAliases = true; dictPanel.ShowVariableNewMenuItem = false; dictPanel.ShowViewDataMenuItem = false;
نام فایل fa.xml میباشد. آنرا در مسیر نرم افزار قرار دهید و سپس کد زیر را اضافه نمایید:
//تنظیم زبان به فارسی StiConfig.LoadLocalization("fa.xml");
<Comp1>This is a content coming from the parent</Comp1>
معرفی مفهوم Render Fragment
برای درج محتوای تامین شدهی توسط کامپوننت والد در یک کامپوننت فرزند، از ویژگی به نام Render Fragment استفاده میشود. مثالی جهت توضیح جزئیات آن:
در ابتدا یک کامپوننت والد جدید را در مسیر Pages\LearnBlazor\ParentComponent.razor به صورت زیر تعریف میکنیم:
@page "/ParentComponent" <h1 class="text-danger">Parent Child Component</h1> <ChildComponent Title="This title is passed as a parameter from the Parent Component"> A `Render Fragment` from the parent! </ChildComponent> <ChildComponent Title="This is the second child component"></ChildComponent> @code { }
- سپس دوبار کامپوننت فرضی ChildComponent به همراه پارامتر Title و یک محتوای جدید قرار گرفتهی در بین تگهای آن، در صفحه تعریف شدهاند.
- بار دومی که ChildComponent در صفحه قرار گرفتهاست، به همراه محتوای جدیدی در بین تگهای خود نیست.
برای دسترسی به این کامپوننت از طریق منوی برنامه، مدخل منوی آنرا به کامپوننت Shared\NavMenu.razor اضافه میکنیم:
<li class="nav-item px-3"> <NavLink class="nav-link" href="ParentComponent"> <span class="oi oi-list-rich" aria-hidden="true"></span> Parent/Child Relation </NavLink> </li>
<div> <div class="alert alert-info">@Title</div> <div class="alert alert-success"> @if (ChildContent == null) { <span> Hello, from Empty Render Fragment </span> } else { <span>@ChildContent</span> } </div> </div> @code { [Parameter] public string Title { get; set; } [Parameter] public RenderFragment ChildContent { get; set; } }
- خاصیت عمومی Title که توسط ویژگی Parameter مزین شدهاست، امکان تنظیم مقدار مشخصی را توسط کامپوننت دربرگیرندهی ChildComponent میسر میکند.
- در اینجا پارامتر عمومی دیگری نیز تعریف شدهاست که اینبار از نوع ویژهی RenderFragment است. توسط آن میتوان به محتوایی که در کامپوننت والد ChildComponent در بین تگهای آن تنظیم شدهاست، دسترسی یافت. همچنین اگر این محتوا توسط کامپوننت والد تنظیم نشده باشد، مانند دومین باری که ChildComponent در صفحه قرار گرفتهاست، میتوان با بررسی نال بودن آن، یک محتوای پیشفرض را نمایش داد.
با این خروجی:
روش دیگری برای فراخوانی Event Call Back ها
در قسمت قبل روش انتقال اطلاعات را از کامپوننتهای فرزند، به والد مشاهده کردیم. فراخوانی آنها در سمت Child Component نیاز به یک متد اضافی داشت و همچنین تنها یک پارامتر را هم ارسال کردیم. برای ساده سازی این عملیات از روش زیر نیز میتوان استفاده کرد:
<button class="btn btn-danger" @onclick="@(() => OnClickBtnMethod.InvokeAsync((1, "A message from child!")))"> Show a message from the child! </button> @code { // ... [Parameter] public EventCallback<(int, string)> OnClickBtnMethod { get; set; } }
- همچنین فراخوانی OnClickBtnMethod.InvokeAsync را نیز در محل تعریف onclick@ بدون نیازی به یک متد اضافی، مشاهده میکنید. نکتهی مهم آن، قرار دادن این قطعه کد داخل ()@ است تا ابتدا و انتهای کدهای #C مشخص شود؛ وگرنه کامپایل نمیشود.
در سمت کامپوننت والد برای دسترسی به OnClickBtnMethod که اینبار یک tuple را ارسال میکند، میتوان به صورت زیر عمل کرد:
@page "/ParentComponent" <h1 class="text-danger">Parent Child Component</h1> <ChildComponent OnClickBtnMethod="ShowMessage" Title="This title is passed as a parameter from the Parent Component"> A `Render Fragment` from the parent! </ChildComponent> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> @code { string MessageText = ""; private void ShowMessage((int Value, string Message) args) { MessageText = args.Message; } }
امکان تعریف چندین RenderFragment
تا اینجا یک RenderFragment را در کامپوننت فرزند تعریف کردیم. امکان تعریف چندین RenderFragment در ChildComponent.razor نیز وجود دارند:
@code { // ... [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public RenderFragment DangerChildContent { get; set; } }
<ChildComponent OnClickBtnMethod="ShowMessage" Title="This title is passed as a parameter from the Parent Component"> <ChildContent> A `Render Fragment` from the parent! </ChildContent> <DangerChildContent> A danger content from the parent! </DangerChildContent> </ChildComponent>
از آنجائیکه ذکر این تگها اختیاری است، نیاز است در ChildComponent.razor بر اساس null بودن آنها، تصمیم به رندر محتوایی پیشفرض گرفت:
@if(DangerChildContent == null) { @if (ChildContent == null) { <span> Hello, from Empty Render Fragment </span> } else { <span>@ChildContent</span> } } else { <span>@DangerChildContent</span> }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-08.zip