برپایی ساختار ابتدایی پروژهی تمرین
ابتدا یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app sample-05 > cd sample-05 > npm start
> npm install --save bootstrap > npm install --save font-awesome
در ادامه نیاز است فایلهای CSS این کتابخانهها و قلمهای وب را import کنیم. به همین جهت ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css"; import "font-awesome/css/font-awesome.css";
همچنین به فایل index.css هم مراجعه کرده و یک padding را به بالای صفحه اضافه میکنیم؛ تا اطلاعات نمایش داده شده، با کمی فاصله از لبهی مرورگر رندر شوند:
body { margin: 0; padding: 20px 0 0 0; font-family: sans-serif; }
پس از نصب و import این کتابخانههای ثالث، به فایل App.js مراجعه کرده و کلاس container اصلی بوت استرپ را در آن تعریف میکنیم تا در برگیرندهی محتوای برنامه شود:
return ( <main className="container"> <h1>Hello world!</h1> </main> );
معرفی سرویسهای دادهی برنامه
کدهای نهایی این قسمت را از فایل پیوست شدهی در انتهای مطلب، میتوانید دریافت کنید. در اینجا یک پوشهی src\services تعریف شدهاست که داخل آن دو فایل fakeGenreService.js و fakeMovieService.js قرار دارند. این فایلها، منبع دادهی درون حافظهای مثال تمرین ما هستند.
سرویس fakeGenre چنین ساختاری را دارد و ژانرهای سینمایی، مانند اکشن، کمدی و غیره در آن لیست شدهاند:
export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, // ... ];
و سرویس fakeMovie که دارای ساختار کلی زیر است، لیست 9 فیلم سینمایی را به همراه دارد:
const movies = [ { _id: "5b21ca3eeb7f6fbccd471815", title: "Terminator", genre: { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: "2018-01-03T19:04:28.809Z" }, //... ];
ایجاد کامپوننت Movies برای نمایش لیست فیلمها در برنامه
اکنون میخواهیم یک کامپوننت جدید را به نام Movies در فایل جدید src\components\movies.jsx ایجاد کنیم، تا لیست فیلمهای سرویس fakeMovieService را نمایش دهد. برای اینکار مراحل زیر را طی خواهیم کرد:
- نمایش سادهی لیست فیلمها توسط یک جدول. برای دریافت لیست اشیاء موجود در fakeMovieService، از متد ()getMovies آن میتوان استفاده کرد.
- اضافه کردن یک دکمهی حذف، به هر ردیف، به نحوی که با کلیک بر روی آن، آن ردیف حذف شود.
- نمایش یک پیام بالای جدول که تعداد فیلمهای موجود در سرویس درون حافظهای را نمایش میدهد. همچنین پس از حذف تمام ردیفها، باید پیام «فیلمی موجود نیست» را نمایش دهد.
پس از ایجاد فایل خالی جدید movies.jsx در پوشهی جدید components، با استفاده از «simple react snippets» نصب شدهی در VSCode، یکبار imrc را تایپ کرده (مخفف import react component است) و سپس دکمهی tab را فشار میدهیم، در آخر اینکار را برای cc نیز تکرار میکنیم (مخفف create class است) تا importها و سپس ساختار ابتدایی کامپوننت React ما تشکیل شوند. نام این کامپوننت را هم Movies که با حرف بزرگ شروع میشود، وارد میکنیم.
اکنون مجددا به App.js مراجعه میکنیم و بجای Hello world ای که نمایش دادیم، کامپوننت Movies را اضافه میکنیم. برای این منظور ابتدا import آنرا به ابتدای فایل اضافه میکنیم:
import Movies from "./components/movies";
return ( <main className="container"> <Movies /> </main> );
دریافت لیست اشیاء فیلمها از سرویس fakeMovieService
برای دریافت لیست اشیاء فیلمها، ابتدا تعریف سرویس آنرا به ابتدای کامپوننت Movies اضافه میکنیم:
import { getMovies } from "../services/fakeMovieService";
سپس خاصیت state را جهت تعریف خاصیت movies که با متد ()getMovies سرویس fakeMovieService مقدار دهی میشود، به نحو زیر تکمیل میکنیم:
state = { movies: getMovies() };
نمایش لیست فیلمها، به همراه مدیریت حذف هر ردیف
در ادامه، کدهای کامل و تکمیل شدهی این کامپوننت را ملاحظه میکنید:
import React, { Component } from "react"; import { getMovies } from "../services/fakeMovieService"; class Movies extends Component { state = { movies: getMovies() }; handleDelete = movie => { const movies = this.state.movies.filter(m => m._id !== movie._id); this.setState({ movies }); }; render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; return ( <React.Fragment> <p>Showing {count} movies in the database.</p> <table className="table"> <thead> <tr> <th>Title</th> <th>Genre</th> <th>Stock</th> <th>Rate</th> <th /> </tr> </thead> <tbody> {this.state.movies.map(movie => ( <tr key={movie._id}> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td>{movie.numberInStock}</td> <td>{movie.dailyRentalRate}</td> <td> <button onClick={() => this.handleDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> </td> </tr> ))} </tbody> </table> </React.Fragment> ); } } export default Movies;
همانطور که در ابتدای بحث نیز ذکر شد، هدف از این تمرین، مرور قسمتهای قبل است و تمام نکات زیر را در قسمتهای پیشین، با جزئیات بیشتری بررسی کردهایم:
- ابتدا خاصیت state و سپس خاصیت movies شیء منتسب به آن، با لیست فیلمهای موجود در سرویس مرتبط، مقدار دهی شدهاند.
- سپس در ابتدای متد render، کار رندر شرطی انجام شدهاست. اگر تعداد فیلمهای دریافتی صفر بود، پیام «فیلمی در بانک اطلاعاتی موجود نیست» نمایش داده میشود و در غیراینصورت، جدول اصلی بوت استرپی برنامه رندر خواهد شد.
در اینجا چون از خاصیت طول آرایهی فیلمها در چندین قسمت قرار است استفاده شود، آنرا توسط Object Destructuring به یک متغیر نسبت دادهایم. همچنین توسط یک نام مستعار هم خاصیت length را با نام جدید count استفاده میکنیم.
- در ادامه بازگشت React.Fragment را مشاهده میکنید. علت اینجا است که نمیخواهیم div اضافهتری را در UI رندر کنیم. React.Fragment سبب میشود تا بتوانیم چندین فرزند را به المان جاری تبدیل شدهی به کدهای جاوا اسکریپتی اضافه کنیم، بدون اینکه خودش به المانی ترجمه شود.
- پس از return، یک () قابل مشاهدهاست. چون خروجی return ما چند سطری است، اگر در سطری که return قرار میگیرد، اطلاعاتی درج نشود، موتور جاوا اسکریپت آنرا با یک سمیکالن خاتمه خواهد داد! و دیگر سطرهای بعدی دیده نمیشوند و پردازش نخواهند شد. به همین جهت از روش ذکر یک () پس از return در فایلهای jsx زیاد استفاده میشود.
- در ابتدای return، همان خاصیت count را نمایش میدهیم.
- سپس کار رندر جدول اصلی برنامه که با کلاسهای جداول بوت استرپ نیز مزین شده، انجام شدهاست. در React برای عدم تداخل ویژگی class با نام از پیش رزرو شدهی class، از خاصیت className برای ذکر کلاسهای CSSای استفاده میشود.
- قسمت thead این جدول مشخص است و سرستونهای جدول را مشخص میکند.
- پس از آن نیاز است ردیفهای جدول را رندر کنیم. اینکار را توسط متد Array.map، با نگاشت هر آیتم آرایهی this.state.movies، به یک tr جدول انجام دادهایم.
- React برای اینکه بتواند DOM مجازی خودش را کنترل کند، نیاز دارد عناصر موجود در آنرا به صورت منحصربفردی تشخص دهد. به همین جهت در اینجا ذکر key را بر روی المان tr که با movie._id مقدار دهی شدهاست، مشاهده میکنید.
- رندر مقادیر سلولهای ردیفها توسط درج {} و سپس ذکر مقداری از شیء movie دریافتی توسط متد Array.map انجام میشود.
- در اینجا ستون رندر دکمهی Delete را نیز مشاهده میکنید. برای مدیریت this در آن و دسترسی به شیء movie جاری (ارسال پارامتر به رویداد گردان آن) و همچنین دسترسی به شیء this کلاس جاری برای کار با آرایهی this.state.movies، از روش arrow functions برای تعریف رویدادگردان onClick استفاده کردهایم.
- در متد handleDelete، یک آرایهی جدید را که id ردیفهای آن با id شیء ردیف انتخابی یکی نیست، بازگشت میدهیم. انتساب این آرایهی جدید به آرایهی this.state.movies، تغییری را در برنامههای React ایجاد نمیکند. در اینجا باید توسط متد this.setState که از کلاس پایهی extends Component دریافت میشود، خاصیت movies را بازنویسی کرد تا React از تغییرات مطلع شده و DOM مجازی جدیدی را با مقایسهی با نمونه جدید، محاسبه کرده و به DOM اصلی، جهت به روز رسانی UI اعمال کند.
- البته در اینجا this.setState({ movies }) را بجای this.setState({ movies: movies }) مشاهده میکنید. علت اینجا است که اگر عبارات key و value یکی باشند، میتوان تنها همان عبارت key را جهت حذف تکرار واژهها، ذکر کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-05.zip
ابزاری سورس باز جهت رندر Html به PDF
برای تعریف المانهای فرمها نیاز است ویژگیهای قابل توجهی را مانند placeholder ،required ،maxlength و غیره، تعریف کرد که در صورت زیاد بودن تعداد المانهای یک فرم، مدیریت تعریف این ویژگیها مشکل میشود. به همین جهت قابلیت ویژهای مخصوص اینکار به نام Attribute Splatting در Blazor درنظر گرفته شدهاست. برای توضیح آن، ابتدا کامپوننت والد Pages\LearnBlazor\AttributeSplatting.razor و کامپوننت فرزند Pages\LearnBlazor\LearnBlazorComponents\AttributeSplattingChild.razor را ایجاد میکنیم.
در کامپوننت فرزند یا همان AttributeSplattingChild، یک المان را به همراه تعدادی ویژگی تعریف شده مشاهده میکنید:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" placeholder="@Placeholder" required="@Required" maxlength="@MaxLength" class="form-control" /> </div> @code { [Parameter] public string Placeholder { get; set; } = "Initial Text"; [Parameter] public string Required { get; set; } = "required"; [Parameter] public string MaxLength { get; set; } = "10"; }
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild Placeholder="Enter the Room Name From Parent" MaxLength="5"> </AttributeSplattingChild>
مشکل! کامپوننت AttributeSplattingChild که فقط به همراه یک المان است، تا این لحظه نیاز به تعریف سه پارامتر جدید را جهت تامین ویژگیهای آن المان داشتهاست. اگر تعداد این المانها افزایش پیدا کرد، آیا راه بهتری برای مدیریت تعداد بالای ویژگیهای مورد نیاز وجود دارد؟
پاسخ: در یک چنین حالتی میتوان ویژگیهای هر المان را توسط پارامتری از نوع Dictionary مدیریت کرد؛ بجای تعریف تک تک آنها به صورت خواصی مجزا. به این قابلیت، Attribute Splatting میگویند.
در این حالت تمام کدهای AttributeSplattingChild.razor به صورت زیر خلاصه میشوند:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" @attributes="InputAttributes" class="form-control" /> </div> @code { [Parameter] public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object> { { "required" , "required"}, { "placeholder", "Initial Text"}, { "maxlength", 10} }; }
و همچنین در ادامه کامپوننت والد یا AttributeSplatting.razor نیز به صورت زیر تغییر میکند:
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild InputAttributes="InputAttributesFromParent"></AttributeSplattingChild> @code{ Dictionary<string, object> InputAttributesFromParent = new Dictionary<string, object> { { "required" , "required"}, { "placeholder", "Enter the Room Name From Parent"}, { "maxlength", 5} }; }
ساده سازی روش تعریف key/valueهای شیء دیکشنری Attribute Splatting
تا اینجا موفق شدیم تعداد قابل ملاحظهای از پارامترهای عمومی یک کامپوننت را تنها توسط یک شیء Dictionary مدیریت کنیم. همچنین همانطور که ملاحظه میکنید، هم Dictionary سمت کامپوننت فرزند و هم سمت کامپوننت والد، نیاز به مقدار دهی اولیهای را دارند. این مقدار دهی اولیه را میتوان به نحو دیگری نیز در حین استفادهی از قابلیت Attribute Splatting، انجام داد:
<div> <h4 class="text-primary pt-3">Attribute Splatting Child Component</h4> <input id="roomName" @attributes="InputAttributes" placeholder="Initial Text" class="form-control" /> </div> @code { [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> InputAttributes { get; set; } = new Dictionary<string, object>(); }
پس از این تغییر، کامپوننت والد هم به صورت زیر خلاصه میشود و دیگر نیازی به تعریف و مقدار دهی InputAttributes و یا تعریف مجزای یک دیکشنری را ندارد. در اینجا هر ویژگی که به المان نسبت داده شود، به عنوان Unmatched Values تفسیر شده و مورد استفاده قرار میگیرد.
@page "/AttributeSplatting" <h1>Attribute Splatting</h1> <AttributeSplattingChild placeholder="Placeholder default"></AttributeSplattingChild>
اگر به تصویر فوق دقت کنید، هرچند در کامپوننت والد مقدار placeholder، به متن دیگری تنظیم شده، اما متن تنظیم شدهی در کامپوننت فرزند، تقدم بیشتری پیدا کرده و نمایش داده شدهاست. علت اینجا است که محل قرارگیری آن در مثال فوق، در سمت راست دایرکتیو attributes@ است. اگر آنرا در سمت چپ attributes@ قرار دهیم، حق تقدم attributes@ بیشتر شده و مقدار تنظیم شدهی در سمت کامپوننت والد، بجای placeholder اولیهی تعریف شدهی در اینجا مورد استفاده قرار میگیرد:
<input id="roomName" placeholder="Initial Text" @attributes="InputAttributes" class="form-control" />
روش انتقال پارامترها به چندین زیر سطح
در قسمت قبل، ParentComponent.razor و ChildComponent.razor را تعریف و تکمیل کردیم. هدف از آنها، بررسی ویژگی Render Fragmentها بود. در ادامهی آن، یک زیر کامپوننت دیگر را نیز به نام Pages\LearnBlazor\LearnBlazorComponents\GrandChildComponent.razor اضافه میکنیم. هدف این است که کامپوننت Parent، کامپوننت Child را فراخوانی کند و کامپوننت Child، کامپوننت GrandChild را تا یک سلسله مراتب از کامپوننتها را تشکیل دهیم.
محتوای GrandChildComponent را هم بسیار ساده نگه میداریم، تا پارامتری رشتهای را دریافت کرده و نمایش دهد:
<div class="row"> <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4> <br /> <p> There is a message - @MessageForGrandChild </p> </div> @code { [Parameter] public string MessageForGrandChild { get; set; } }
<div class="mt-2"> <GrandChildComponent MessageForGrandChild="@MessageForGrandChild"></GrandChildComponent> </div> @code { [Parameter] public string MessageForGrandChild { get; set; } // ... }
<ChildComponent MessageForGrandChild="This is a message from Grand Parent" Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent>
بنابراین اکنون این سؤال مطرح میشود که آیا میتوان پارامتری را در همان کامپوننت Parent تعریف کرد که توسط کامپوننت GrandChild قابل شناسایی و استفاده باشد، بدون اینکه کامپوننت Child را در این بین تغییر دهیم؟
پاسخ: بله. برای اینکار ویژگیهای CascadingValue و CascadingParameter در Blazor پیش بینی شدهاند.
در ابتدا، پارامتر MessageForGrandChild کامپوننت Child حذف کرده و سپس آنرا توسط کامپوننت توکار CascadingValue محصور میکنیم. در اینجا نیاز است مقدار انتقالی را نیز مشخص کنیم:
<CascadingValue Value="@MessageForGrandChild"> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> </CascadingValue> @code { string MessageForGrandChild = "This is a message from Grand Parent";
<GrandChildComponent></GrandChildComponent>
[CascadingParameter] public string MessageForGrandChild { get; set; }
چند نکته:
- در اینجا نوع CascadingParameter تعریف شده، باید با نوع Value کامپوننت CascadingValue، در بالاترین سطح سلسله مراتب کامپوننتها، یکی باشد.
- نام CascadingParameter تعریف شده مهم نیست. فقط نوع آن مهم است.
- تمام کامپوننتهای موجود و پوشش داده شدهی در سلسله مراتب جاری، قابلیت تعریف CascadingParameter ای مانند مثال فوق را دارند و این تعریف، محدود به پایینترین سطح موجود نیست. برای مثال در اینجا در کامپوننت Child هم در صورت نیاز میتوان همین CascadingParameter را تعریف و استفاده کرد.
روش تعریف پارامترهای آبشاری نامدار
تا اینجا روش انتقال یک پارامتر را از بالاترین سطح، به پایینترین سطح سلسله مراتب کامپوننتهای تعریف شده، بررسی کردیم. اکنون شاید این سؤال مطرح شود که اگر خواستیم بیش از یک پارامتر را بین اجزای این سلسله، به اشتراک بگذاریم چه باید کرد؟
در این حالت میتوان پارامتر جدید را توسط یک کامپوننت CascadingValue تو در تو، به صورت زیر معرفی کرد؛ که اینبار نامدار نیز هست:
<CascadingValue Value="@MessageForGrandChild" Name="MessageFromGrandParent"> <CascadingValue Value="@Number" Name="GrandParentsNumber"> <ChildComponent Title="This is the second child component"> <p><b>@MessageText</b></p> </ChildComponent> </CascadingValue> </CascadingValue> @code { string MessageForGrandChild = "This is a message from Grand Parent"; int Number = 7;
پس از این تغییر، GrandChildComponent، این پارامترهای نامدار را از طریق ذکر صریح خاصیت Name ویژگی CascadingParameter، دریافت میکند:
<div class="row"> <h4 class="text-primary pl-4 pt-2 col-12">Grand Child Component</h4> <br /> There is a message: @Message <br /> GrandParentsNumber: @Number </div> @code { [CascadingParameter(Name = "MessageFromGrandParent")] public string Message { get; set; } [CascadingParameter(Name = "GrandParentsNumber")] public int Number { get; set; } }
یک نکته: چون نوع پارامترهای ارسالی یکی نیست، الزامی به ذکر نام آنها نبود. در این حالت بر اساس نوع پارامترهای آبشاری، عملیات اتصال مقادیر صورت میگیرد. اما اگر نوع هر دو را برای مثال رشتهای تعریف میکردیم، مقدار Number، بر روی مقدار MessageForGrandChild بازنویسی میشد. یعنی در UI، هر دو پارامتر هم نوع، یک مقدار را نمایش میدادند که در حقیقت مقدار پایینترین CascadingValue تعریف شدهاست. بنابراین ذکر نام پارامترهای آبشاری، روشیاست جهت تمایز قائل شدن بین پارامترهای هم نوع.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-09.zip
⭐️ Course Contents ⭐️
Section 1: Introduction
Section 2: Blazor Files and Folders
Section 3: Blazor - Data and Property Binding
Section 4: Blazor - Shared Components and Event Binding
Section 5: Blazor - Render Fragment, Attribute Splatting and Routing
Section 6: Blazor - JavaScript
Section 7: Blazor Lifecycle
Section 8: Model and Repository
Section 9: Category CRUD
Section 10: Delete Component
آموزش ساخت یک بلاگ با Blazor .Net 8
In this video we will build a complete full stack Blog Web App using the new Blazor Web App with SSR with .Net 8 | Step by Step | From Scratch to End
You will learn: New Static Server Side Rendering with Blazor, QuickGrid, Enhanced Navigation, Enhanced Forms, Stream Rendering, Image Preview & Upload, Identity Authentication, Interactive Server Render Mode for Admin Panel, and much more
دریافت Twitter Bootstrap
محل اصلی دریافت Twitter Bootstrap، آدرس ذیل است:
البته ما از آن در اینجا به شکل خام فوق استفاده نخواهیم کرد؛ زیرا نیاز است قابلیتهای استفاده در محیطهای راست به چپ فارسی نیز به آن اضافه شود. برای این منظور میتوانید از یکی از دو بسته نیوگت ذیل استفاده نمائید:
RTL Twitter Bootstrap, https://nuget.org/packages/Twitter.Bootstrap.RTL
و یا حتی از منابع سایت http://rbootstrap.ir نیز میتوان استفاده کرد.
برای نمونه دستور زیر را در کنسول پاورشل ویژوال استودیو وارد نمائید تا اسکریپتها و فایلهای CSS مورد نیاز به پروژه جاری اضافه شوند:
PM> Install-Package Twitter.BootstrapRTL
در اینجا فایلهای min، نگارشهای فشرده شده فایلهای js یا css هستند که با توجه به امکانات اضافه شده به ASP.NET MVC4، از آنها استفاده نخواهیم کرد و برای افزودن و تعریف آنها از امکانات Bundling and minification توکار فریم ورک ASP.NET MVC به نحوی که در ادامه توضیح داده خواهد شد، استفاده میکنیم.
فایلهای png اضافه شده، آیکونهای مخصوص Twitter Bootstrap هستند که اصطلاحا به آنها sprite images نیز گفته میشود. در این نوع تصاویر، تعداد زیادی آیکون در کنار هم، برای بهینه سازی تعداد بار رفت و برگشت به سرور جهت دریافت تصاویر، طراحی شده و قرار گرفتهاند.
فایلهای js این مجموعه اختیاری بوده و برای استفاده از ویجتهای Twitter Bootstrap مانند آکاردئون کاربرد دارند. این فایلها برای اجرا، نیاز به jQuery خواهند داشت.
افزودن تعاریف اولیه Twitter Bootstrap به یک پروژه ASP.NET MVC
امکانات Bundling and minification در نوع پروژههای نسبتا جامعتر ASP.NET MVC به صورت پیش فرض لحاظ شده است. اما اگر یک پروژه خالی را شروع کردهاید، نیاز است بسته نیوگت آنرا نیز نصب کنید:
PM> Install-Package Microsoft.AspNet.Web.Optimization
using System.Collections.Generic; using System.IO; using System.Web; using System.Web.Optimization; namespace Mvc4TwitterBootStrapTest.Helper { /// <summary> /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are /// included in the order you register them. /// </summary> public class AsIsBundleOrderer : IBundleOrderer { public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files) { return files; } } public static class BundleConfig { private static void addBundle(string virtualPath, bool isCss, params string[] files) { BundleTable.EnableOptimizations = true; var existing = BundleTable.Bundles.GetBundleFor(virtualPath); if (existing != null) return; Bundle newBundle; if (HttpContext.Current.IsDebuggingEnabled) { newBundle = new Bundle(virtualPath); } else { newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify()); } newBundle.Orderer = new AsIsBundleOrderer(); foreach (var file in files) newBundle.Include(file); BundleTable.Bundles.Add(newBundle); } public static IHtmlString AddScripts(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Render(virtualPath); } public static IHtmlString AddStyles(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Render(virtualPath); } public static IHtmlString AddScriptUrl(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Url(virtualPath); } public static IHtmlString AddStyleUrl(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Url(virtualPath); } } }
پس از افزودن کلاسهای کمکی فوق، به فایل layout پروژه مراجعه کرده و تعاریف ذیل را به ابتدای فایل اضافه نمائید:
@using Mvc4TwitterBootStrapTest.Helper <!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> @BundleConfig.AddStyles("~/Content/css", "~/Content/bootstrap.css", "~/Content/bootstrap-responsive.css", "~/Content/Site.css" ) @BundleConfig.AddScripts("~/Scripts/js", "~/Scripts/jquery-1.9.1.min.js", "~/Scripts/jquery.validate.min.js", "~/Scripts/jquery.unobtrusive-ajax.min.js", "~/Scripts/jquery.validate.unobtrusive.min.js", "~/Scripts/bootstrap.min.js" ) @RenderSection("JavaScript", required: false) </head>
<!DOCTYPE html> <html> <head> <title>Index</title> <link href="/Content/css?v=vsUQD0OJg4AJ-RZH8jSRRCu_rjl2U1nZrmSsaUyxoAc1" rel="stylesheet"/> <script src="/Scripts/js?v=GezdoTDiWY3acc3mI2Ujm_7nKKzh6Lu1Wr8TGyyLpW41"></script> </head>
مفاهیم پایهای Twitter Bootstrap
الف) Semantic class names
به عبارتی کلاسهای Twitter Bootstrap دارای نامهایی معنا دار و مفهومی میباشند؛ مانند کلاسهای CSSایی، به نامهای Succes، Error، Info و امثال آن. این نامها مفهومی را میرسانند؛ اما در مورد نحوه پیاده سازی آنها جزئیاتی را بیان نمیکنند.
برای نمونه میتوان کلاسی را به نام redText ایجاد کرد. هر چند این نام، توضیحاتی را در مورد علت وجودیاش بیان میکند، اما بسیار ویژه بوده و در مورد جزئیات پیاده سازی آن نیز اطلاعاتی را ارائه میدهد. در این حالت redText معنایی ندارد. چرا یک Text باید قرمز باشد؟ برای مثال این متن قرمز است چون مثلا شخصی، به آن رنگ ویژه علاقه دارد، یا اینکه قرمز است بخاطر نمایش خطایی در صفحه؟ به همین جهت در Twitter Bootstrap از نامهای مفهومی یاده شده، مانند Error استفاده میشود. نامهایی معنا دار اما بدون دقیق شدن در مورد ریز جزئیات پیاده سازی آنها. در این حالت میتوان قالب جدیدی را تدارک دید و با ارائه تعاریف جدیدی برای کلاس Error و نحوه نمایش دلخواهی را به آن اعمال نمود.
یا برای نمونه نام rightside را برای نمایش ستونی در صفحه، درنظر بگیرید. این نام بسیار ویژه است؛ اما Sematic name آن میتواند sidebar باشد تا بدون دقیق شدن در جزئیات پیاده سازی آن، در چپ یا راست صفحه قابل اعمال باشد.
Semantic class names کلیدهایی هستند جهت استفاده مجدد از قابلیتهای یک فریم ورک CSS.
ب) Compositional classes
اکثر کلاسهای Twitter Bootstrap دارای محدوده کاری کوچکی هستند و به سادگی قابل ترکیب با یکدیگر جهت رسیدن به نمایی خاص میباشند. برای مثال به سادگی میتوان به یک table سه ویژگی color، hover و width برگرفته شده از Twitter Bootstrap را انتساب داد و نهایتا به نتیجه دلخواه رسید؛ بدون اینکه نگران باشیم افزودن کلاس جدیدی در اینجا بر روی سایر کلاسهای انتساب داده شده، تاثیر منفی دارد.
ج) Conventions
برای استفاده از اکثر قابلیتهای این فریم ورک CSS یک سری قراردادهای پیش فرضی وجود دارند. برای مثال اگر از کلاس توکار pagination به همراه یک سری ul و li استفاده کنید، به صورت خودکار یک pager شکیل ظاهر خواهد شد. یا برای مثال اگر به یک html table کلاسهای table table-striped table-hover را انتساب دهیم، به صورت خودکار قراردادهای پیش فرض table مجموعه Twitter Bootstrap به آن اعمال شده، به همراه رنگی ساختن یک درمیان زمینه ردیفها و همچنین فعال سازی تغییر رنگ ردیفها با حرکت ماوس از روی آنها.
طرحبندی صفحات یک سایت به کمک Twitter Bootstrap
بررسی Grid layouts
Layout به معنای طرحبندی و چیدمان محتوا در یک صفحه است. یکی از متداولترین روشهای طرحبندی صفحات چه در حالت چاپی و چه در صفحات وب، چیدمان مبتنی بر جداول و گریدها است. از این جهت که نحوه سیلان و نمایش محتوا از چپ به راست و یا راست به چپ را به سادگی میسر میسازند؛ به همراه اعمال حاشیههای مناسب جهت قسمتهای متفاوت محتوای ارائه شده. Grid در طرحبندی، نمایش بصری نخواهد داشت اما در ساختار صفحه وجود داشته و مباحثی مانند جهت، موقعیت و یکپارچگی و یکدستی طراحی را سبب میشود.
به علاوه مرورگرها و مفهوم Grid نیز به خوبی با یکدیگر سازگار هستند. در دنیای HTML و ،CSS طراحیها بر اساس مفهوم ساختار مستطیلی اشیاء صورت میگیرد:
برای نمونه در اینجا تصویر CSS Box Model را مشاهده میکنید. به این ترتیب، هر المان دارای محدودهای مستطیلی با طول و عرض مشخص، به همراه ویژگیهایی مانند Margin، Border و Padding است.
در سالهای اولیه طراحی وب، عموما کارهای طراحی صفحات به کمک HTML Tables انجام میشد. اما با پختهتر شدن CSS، استفاده از Tables برای طراحی صفحات کمتر و کمتر گشت تا اینکه نهایتا فریم ورکهای CSS ایی پدید آمدند تا طراحیهای مبتنی بر CSS را با ارائه گریدها، سادهتر کنند. مانند Blue print، 960 GS و ... Twitter Bootstrap که طراحی مبتنی بر گریدهای CSS ایی را به مجموعه قابلیتهای دیگر خود افزوده است.
بررسی Fixed Grids
در اینجا در صفحه layout برنامه، یک Div دربرگیرنده دو Div دیگر را مشاهده میکنید:
<body> <div> <div> <h1> Title Title </h1> Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text </div> <div> @RenderBody() </div> </div> </body>
برای اینکار در Twitter Bootstrap از کلاسی به نام row استفاده میشود که بیانگر یک ردیف است. این کلاس را به خارجیترین Div موجود اعمال خواهیم کرد. در یک صفحه، هر تعداد row ایی را که نیاز باشد، میتوان تعریف کرد. داخل این ردیفها، امکان تعریف ستونهای مختلف و حتی تعریف ردیفهای تو در تو نیز وجود دارد. هر ردیف Twitter Bootstrap از 12 ستون تشکیل میشود و برای تعریف آنها از کلاس span استفاده میگردد. در این حالت جمع اعداد ذکر شده پس از span باید 12 را تشکیل دهند.
<body> <div class="row"> <div class="span7"> <h1> Title Title </h1> Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text </div> <div class="span5"> @RenderBody() </div> </div> </body>
در این تصویر، قسمت RenderBody کار رندر اکشن متد Index کنترلر Home برنامه را با Viewایی معادل کدهای ذیل، انجام داده است:
@{ ViewBag.Title = "Index"; } <h2> Index</h2> <div class="hero-unit"> <h2>@ViewBag.Message</h2> <p> This is a template to demonstrate the way to beautify the default MVC template using Twitter Bootstrap website. It is pretty simple and easy.</p> <p> <a href="http://asp.net/mvc" class="btn btn-primary btn-large" style="color: White;"> To learn more about ASP.NET MVC visit »</a></p> </div>
درک نحوه عملکرد Grid در Twitter Bootstrap
در مثال ذیل 5 ردیف را مشاهده میکنید:
<div class="row"> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> </div> <div class="row"> <div class="span3">3</div> <div class="span4">4</div> <div class="span5">5</div> </div> <div class="row"> <div class="span5">5</div> <div class="span7">7</div> </div> <div class="row"> <div class="span3">3</div> <div class="span7 offset2">7 offset 2</div> </div> <div class="row"> <div class="span12">12</div> </div>
در ستون چهارم، از کلاس offset نیز استفاده شده است. این مورد سبب میشود ستون جاری به تعدادی که مشخص شده است به سمت چپ (با توجه به استفاده از حالت RTL در اینجا) رانده شود و سپس ترسیم گردد.
یا اینکه میتوان مانند ردیف آخر، یک ستون را به عرض 12 که در حقیقت 940 پیکسل است، ترسیم نمود.
برای اینکه بتوانیم این گرید تشکیل شده و همچنین ستونها را بهتر مشاهده کنیم، به فایل style.css سایت، تنظیم زیر را اضافه کنید:
[class*="span"] { background-color: lightblue; text-align: center; margin-top: 15px; }
نکته جالب این گرید، Responsive یا واکنشگرا بودن آن است. در این حالت، عرض مرورگر را کم و زیاد کنید. خواهید دید که ستونها در صورتیکه در عرض نمایشی جاری، قابل ارائه نباشند، به ردیفهای بعدی منتقل خواهند شد.
البته باید دقت داشت که این گرید هیچگاه یک ستون را نخواهد شکست. برای نمونه ردیف آخر، همواره با همان عرض ثابتش نمایش داده میشود و با کوچکتر کردن اندازه مرورگر، یک اسکرول افقی برای نمایش محتوای آن ظاهر خواهد شد.
یک نکته
اگر نمیخواهید که چنین رفتار واکنشگرایی بروز کند، نیاز است کلیه ردیفها را در div ایی با کلاسی به نام container محصور کنید.
به این ترتیب ابتدا گرید نمایش داده شده، در میانه صفحه ظاهر خواهد شد (پیشتر از سمت راست شروع شده بود). همچنین دیگر با کوچک و بزرگ شدن اندازه مرورگر، ستونها به شکل یک پشته بر روی هم قرار نخواهند گرفت. (اگر پس از این تنظیم، چنین قابلیتی را مشاهده نکردید و هنوز هم طراحی، واکنشگرا بود، تعریف bootstrap-responsive.css را نیاز است برای آزمایش، از هدر صفحه حذف کنید)
بررسی Fluid Grids
به گرید قسمت قبل از این جهت Fixed Grid گفته میشود که عرض هر span آن با یک عدد مشخص تعیین گشته است. اما در حالت Fluid Grid، عرض هر span برحسب درصد تعیین میشود. بکارگیری درصد در اینجا به معنای امکان تغییر عرض یک ستون بر اساس عرض جاری Container آن است. در اینجا span12 دارای عرض 100 درصد خواهد بود.
در مثال قبل، برای استفاده از Fluid grids، تنها کافی است هرجایی کلاسی مساوی row وجود دارد، به row-fluid تغییر کند. همچنین کلاس container را به container-fluid تغییر دهید.
برای آزمایش آن، اندازه و عرض نمایشی مرورگر خود را تغییر دهید. اینبار مشاهده خواهید کرد که برخلاف حالت Fixed Grid، عرض ستونها به صورت خودکار کم و زیاد میشوند. این مورد بر روی محتوای قرار گرفته در این ستونها نیز تاثیر گذار است. برای مثال اگر یک تصویر را در حالت Fluid grid در ستونی قرار دهید، با تغییر عرض مرورگر، اندازه این تصویر نیز تغییر خواهند کرد؛ اما در حالت Fixed Grid خیر.
حالت Fluid، شیوه متداول استفاده از bootstrap در اکثر سایتهای مهمی است که تابحال از این فریم ورک CSS استفاده کردهاند.
مروری بر طراحی واکنشگرا یا Responsive
این روزها تعدادی از کاربران، با استفاده از ابزارهای موبایل و تبلتها از وب سایتها بازدید میکنند. هر کدام از اینها نیز دارای اندازه نمایشی متفاوتی میباشند. بنابراین نیاز خواهد بود تا حالت بهینهای را جهت اینگونه وسایل نیز طراحی نمود. حالت بهینه در اینجا به معنای قابل خواندن بودن متون، امکان استفاده از لینکهای ورود به صفحات مختلف و همچنین حذف اسکرول و مباحث زوم جهت مشاهده صفحات است.
یکی از ویژگیهای CSS به نام media چنین قابلیتی را فراهم میسازد. برای نمونه قسمتی از فایل bootstrap-responsive.css دارای چنین تعاریفی است:
@media (min-width: 768px) and (max-width: 979px) { .hidden-desktop { display: inherit !important; }
Bootstrap برای مدیریت اندازههای مختلف وسایل موبایل، شیوهنامههای خاصی را تدارک دیده است که از اندازه px480 و یا کمتر، تا px1200 و یا بیشتر را پوشش میدهد.
به این ترتیب با اندازه px940 که پیشتر در مورد آن بحث شد، اندازه span12 به صورت خودکار به اندازههای متناسب با صفحات نمایشی کوچکتر تنظیم میگردد. همچنین برای اندازههای صفحات کوچکتر از 768px به صورت خودکار از Fluid columns استفاده میگردد.
تنها کاری که برای اعمال این قابلیت باید صورت گیرد، افزودن تعاریف فایل bootstrap-responsive.css به هدر صفحه است که در قسمت قبل انجام گردید. این فایل باید پس از فایل اصلی bootstrap.css اضافه شود.
کلاسهای کمکی طراحی واکنشگرا
Bootstrap شامل تعدادی کلاس کمکی در فایل bootstrap-responsive.css خود میباشد شامل visible-phone، visible-tablet و visible-desktop به علاوه hidden-phone، hidden-tablet و hidden-desktop. به این ترتیب میتوان محتوای خاصی را جهت وسایل ویژهای نمایان یا مخفی ساخت.
برای مثال محتوای مشخص شده با کلاس hidden-desktop، در اندازه وسایل دسکتاپ نمایش داده نخواهد شد.
برای آزمایش آن، شش div را با کلاسهای یاد شده و محتوایی دلخواه ایجاد کرده و سپس اندازه عرض مرورگر را تغییر دهید تا بهتر بتوان مخفی یا نمایان ساختن محتوا را بر اساس اندازه صفحه جاری درک کرد.
یکی از کاربردهای این قابلیت، قرار دادن تبلیغاتی با اندازههای تصاویری مشخص برای وسایل مختلف است؛ بجای اینکه منتظر شویم تا Fluid layout اندازه تصاویر را به صورت خودکار کوچک یا بزرگ کند، که الزاما بهترین کیفیت را حاصل نخواهد ساخت.
<div class="container-fluid"> <div class="row-fluid"> <div class="span4"> <div class="visible-phone"> visible-phone</div> </div> <div class="span4"> <div class="visible-tablet"> visible-tablet</div> </div> <div class="span4"> <div class="visible-desktop"> visible-desktop</div> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span4"> <div class="hidden-phone"> hidden-phone</div> </div> <div class="span4"> <div class="hidden-tablet"> hidden-tablet</div> </div> <div class="span4"> <div class="hidden-desktop"> hidden-desktop</div> </div> </div> </div>
بررسی تغییرات Blazor 8x - قسمت هشتم - مدیریت انتقال اطلاعات Pre-Rendering سمت سرور، به جزایر تعاملی
بررسی نحوهی عملکرد سرویس PersistentComponentState
سرویس PersistentComponentState، در داتنت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که بهنظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامههای Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
<component type="typeof(App)" render-mode="ServerPrerendered" />
<body> <component type="typeof(App)" render-mode="WebAssemblyPrerendered" /> <persist-component-state /> </body>
@page "/" @implements IDisposable @inject PersistentComponentState applicationState @inject IUserRepository userRepository @foreach(var user in users) { <ShowUserInformation user="@item"></ShowUserInformation> } @code { private const string cachingKey = "something_unique"; private List<User> users = new(); private PersistingComponentStateSubscription persistingSubscription; protected override async Task OnInitializedAsync() { persistingSubscription = applicationState.RegisterOnPersisting(PersistData); if (applicationState.TryTakeFromJson<List<User>>(cachingKey, out var restored)) { users = restored; } else { users = await userRepository.GetAllUsers(); } } public void Dispose() { persistingSubscription.Dispose(); } private Task PersistData() { applicationState.PersistAsJson(cachingKey, users); return Task.CompletedTask; } }
- ابتدا تزریق سرویس PersistentComponentState را مشاهده میکنید. این همان کامپوننتی است که کار کش کردن اطلاعات را مدیریت میکند.
- سپس فراخوانی متد RegisterOnPersisting انجام شدهاست. متدی که توسط آن ثبت میشود، در حین عملیات pre-rendering فراخوانی میشود تا اطلاعاتی را کش کند. نحوهی این کش شدن را در ادامهی مطلب بررسی میکنیم.
- سپس فراخوانی متد TryTakeFromJson رخدادهاست. اگر این متد اطلاعاتی را برگرداند، یعنی pre-rendering سمت سرور پیشتر انجام شده و اطلاعاتی کش شده، برای دریافت در سمت کلاینت، وجود داشته و نیازی به مراجعهی مجدد و دوباره، به بانک اطلاعاتی نیست.
- دراینجا یک قسمت Dispose را هم مشاهده میکنید تا اگر کاربر به صفحهی دیگری مراجعه کرد، متد ثبت شدهی در اینجا، از لیست مواردی که باید در حین pre-rendering سریالایز شوند، حذف گردد.
اگر از این روش استفاده نشود، به علت دوبار فراخوانی شدن متد OnInitializedAsync یک کامپوننت به همراه pre-rendering، اطلاعاتی که در حین pre-rendering کامپوننت از بانک اطلاعاتی، برای تولید محتوای استاتیک صفحه در سمت سرور دریافت شده، با فعالسازی آتی تعاملی سمت کلاینت آن کامپوننت، از دست خواهد رفت و در این حالت کلاینت باید مجددا این اطلاعات را از بانک اطلاعاتی درخواست کند. بنابراین هدف از persist-component-state، حفظ دادهها در بین دو رندر است؛ رندر اولی که در سمت سرور انجام میشود و رندر دومی که متعاقبا در سمت کلاینت رخ میدهد.
یک نکته: به یک چنین قابلیتی در فریمورکهای دیگر «hydration» هم گفته میشود که در اصل یک شیء دیکشنری است که مقدار شیء حالت را در سمت سرور دریافت کرده و آنرا در زمان فعال شدن سمت کلاینت کامپوننت، برای استفادهی مجدد مهیا میکند.
سؤال: اطلاعات سرویس PersistentComponentState کجا ذخیره میشوند؟
همانطور که در مثال فوق هم مشاهده کردید، سرویس PersistentComponentState، این state را به صورت JSON ذخیره میکند. اما ... این اطلاعات دقیقا کجا ذخیره میشوند؟
برای مشاهدهی آنها، فقط کافی است به source صفحه مراجعه کنید تا با دو مقدار مخفی رمزنگاری شدهی زیر در انتهای HTML صفحه مواجه شوید!
<!--Blazor-Server-Component-State:CfDJXyz-> <!--Blazor-WebAssembly-Component-State:eyJVc2Xyz->
مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام دادهاست! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال میشوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده میشوند و هدف آنها ارسال اطلاعاتی به سمت سرور نیست.
یک نکته: همانطور که عنوان شد، در نگارشهای قبلی Blazor، از تگهلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده میکردند. استفاده از این تگهلپر در Blazor 8x منسوخ شده و به صورت خودکار دادههای تمام سرویسهای PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره میکنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج میکنند.
روش خاموش کردن Pre-rendering
برای خاموش کردن pre-rendering نیاز است پارامتر همنامی را به نحو زیر با false مقدار دهی کرد:
@rendermode renderMode @code { private static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false); }
بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState
در قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام میشود؛ یکبار در زمان pre-rendering و با مراجعهی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آنرا از بانک اطلاعاتی دریافت نکنیم، راهحل سوم آن، استفاده از سرویس PersistentComponentState است.
در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده میکنید که کمی پیچیدهتر شدهاست. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده میشوند و نه پس از کلیک بر روی دکمهای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیشرندر سمت سرور اجرا میشود و بار دیگر زمانیکه وباسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال میشود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شدهی در حین pre-rendering ابتدایی رخدادهی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وباسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده میشوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.
@implements IDisposable @inject IProductStore ProductStore @inject PersistentComponentState PersistentState <h3>Related products</h3> @if (_relatedProducts == null) { <p>Loading...</p> } else { <div class="mt-3"> @foreach (var item in _relatedProducts) { <a href="/ProductDetails/@item.Id"> <div class="col-sm"> <h5 class="mt-0">@item.Title (@item.Price)</h5> </div> </a> } </div> } @code{ private const string StateCachingKey = "products"; private IList<Product>? _relatedProducts; private PersistingComponentStateSubscription _persistingSubscription; [Parameter] public int ProductId { get; set; } protected override async Task OnInitializedAsync() { _persistingSubscription = PersistentState.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); if (PersistentState.TryTakeFromJson<IList<Product>>(StateCachingKey, out var restored)) { _relatedProducts = restored; } else { await Task.Delay(500); // Simulates asynchronous loading _relatedProducts = await ProductStore.GetRelatedProducts(ProductId); } } private Task OnPersistingAsync() { PersistentState.PersistAsJson(StateCachingKey, _relatedProducts); return Task.CompletedTask; } public void Dispose() { _persistingSubscription.Dispose(); } }
نکتهی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیهی کامپوننت درست کار میکنند. یعنی اگر نگارش وباسمبلی کامپوننت پس از وقوع پیشرندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعهی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در قسمت ششم این سری بررسی کردیم ... دیگر کار نمیکنند و در این حالت کش شدنی رخ نمیدهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفادهی از PersistentComponentState را زیر سؤال میبرد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمیشود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمیخواند). اطلاعات بیشتر
Pipeهای Pure و Impure
Pipeها کلا در دو دستهی Pure و Impure قرار میگیرند. هنگام ساخت Pipe سفارشی در صورتیکه نوع Pipe مشخص نشود، به صورت پیش فرض از نوع Pure خواهد بود. برای تعریف Pipeهایی از نوع Impure کافی است در متادیتای Pipe@، پرچم Pure را به مقدار false تنظیم کنید.
@Pipe({ name: 'impurePipe', pure: false })
Pure Pipe
این نوع Pipeها تنها زمانی فراخوانی مجدد میشوند که یک تغییر محض (Pure Change) بر روی عبارت ورودی آنها رخ دهد. هر نوع تغییری بر روی عبارات ورودی از جنس string ، number ، Boolean ، Symbol و عبارات اولیه، یا هرنوع تغییری در ارجاع یک شیء مانند Date ، Array ، Function و Object نیز تغییر محض محسوب میشود. به عنوان مثال هیچکدام از تغییرات زیر یک تغییر محض محسوب نمیشوند:
numbers.push(10); obj.name = ‘javad’;
حالا میتوان به این نتیجه رسید که اضافه شدن مقداری به آرایه یا بهروزرسانی یک property از object، باعث فراخوانی مجدد Pure Pipe نخواهد نشد. شاید این نوع از Pipeها محدود کننده باشند، اما بسیار سریع هستند (برسی تغییر در ارجاع یک شیء بسیار سریعتر از بررسی کامل یک شیء، صورت میگیرد).
Impure Pipe
این نوع Pipeها در اغلب رخدادهای کامپوننت از جمله فشره شدن کلید یا حرکت ماوس و رخدادهای دیگر فراخوانی مجدد میشوند. با در نظر گرفتن این نگرانی، هنگام پیاده سازی این نوع Pipeها باید مراقب بود؛ زیرا این نوع Pipeها با اجرای طولانی خود میتوانند رابط کاربری شما را نابود کنند. برای درک کامل تفاوت این دو نوع از Pipeها مثالی را دنبال میکنیم.
مثال: قصد داریم Pipe سفارشی را پیاده سازی کنیم تا آرایهای از اعداد را دریافت و فقط اعداد زوج را فیلتر کرده و نمایش دهد.
برای این منظور یک فایل جدید را با نام even-numbers.pipe.ts با محتویات زیر ایجاد میکنیم:
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'evenNumbers' }) export class EvenNumbersPipe implements PipeTransform { transform(numbers: Array<number>): Array<number> { var x=numbers.filter(r => r % 2 == 0); return x; } }
// . . . import { EvenNumbersPipe } from './pipes/even-numbers.pipe' @NgModule({ declarations: [ . . . EvenNumbersPipe ], . . . }) export class AppModule { }
سپس در کامپوننت مورد نظر خود متغیری را به نام numbers از نوع آرایه، با مقدار اولیهی اعداد از یک تا ده، تعریف میکنیم:
numbers: Array<number> = [1,2,3,4,5,6,7,8,9,10];
<h1>All numbers</h1> <span *ngFor="let number of numbers"> {{number}} </span>
<p> <input type="text" #number /> <input type="button" (click)="numbers.push(number.value)" value="Add number"/> </p>
تگهای زیر را نیز برای اعمال Pipe نمایش اعداد زوج، به قالب کامپوننت اضافه میکنیم:
<h1>even numbers</h1> <span *ngFor="let number of numbers | evenNumbers"> {{number}} </span>
برای حل این مشکل، هنگام اضافه شدن عدد به آرایه، اگر ارجاع آرایه را تغییر دهیم، Pure Pipe متوجه تغییرات خواهد شد و لیست اعداد را بهروز رسانی میکند (تغییر در ارجاع یک شیء، از نوع تغییرات محض است):
<p> <input type="text" #number /> <input type="button" (click)="numbers = numbers.concat(number.value)" value="Add number"/> </p>
@Pipe({ name: 'evenNumbers', pure: false }) export class EvenNumbersPipe implements PipeTransform { //… }
کسانیکه با Angular 1.x آشنایی دارند، شاید اکنون متوجه این شدهاند که چرا در Angular به مشابه Angular 1.x دیگر خبری ازfilter و orderBy نیست. با توجه به اینکه این دو فیلتر فقط با عبارات از نوع object سروکار داشتند، پیادهسازی آنها فقط با Impure Pipeها امکان پذیر بود و با توجه به اینکه Impure Pipeها در هر بار چرخه تغییرات کامپوننت اجرا خواهند شد، باعث کندی در صفحات خواهند شد.