مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت هفدهم
در قسمت قبل بحث Style و Font را بررسی کردیم. در این قسمت قصد بررسی Animationها را داریم. Animation خود دارای دو قسمت است:
1- استفاده از Xamanimation 
2- استفاده از Lottie
Xamanimation به شما کمک می‌کند تا در Xaml، انیمیشن‌های خود را تعریف کنید. پس از نصب Package مربوطه، می‌توانید مثال زیر را تست کنید:
<Button
    x:Name="DeleteButton"
    BackgroundColor="Orange"
    Text="Delete">
    <Button.Triggers>
        <EventTrigger Event="Clicked">
            <bitView:SetPropertyAction Property="BackgroundColor" Value="Red" />
            <xamAnimation:BeginAnimation>
                <xamAnimation:BeginAnimation.Animation>
                    <xamAnimation:ColorAnimation
                        Target="{x:Reference DeleteButton}"
                        ToColor="Orange"
                        Duration="1000" />
                </xamAnimation:BeginAnimation.Animation>
            </xamAnimation:BeginAnimation>
        </EventTrigger>
    </Button.Triggers>
</Button>
در این مثال یک دکمه داریم که وقتی روی آن Click می‌کنیم، EventTrigger مربوطه اجرا می‌شود و ابتدا با کمک SetPropertyAction مقدار BackgroundColor دکمه روی Red تعیین می‌شود. سپس با Animation، از رنگ Red به Orange می‌رسیم؛ در طول 1000 میلی ثانیه. البته درست کردن Animation احتیاج به خلاقیت هم دارد(!) و این مثال فقط جنبه‌ی تکنیکی ماجرا را به شما توضیح می‌دهد. برای مشاهده مثال‌های بیشتر به سایت مربوطه مراجعه کنید.

در برخی مواقع Animationها به سادگی تغییر رنگ و Opacity و موقعیت اشیاء نیستند؛ اگر چه با ترکیب کردن همانها هم می‌توان کلی کارهای جالب کرد. برای انیمیشن‌های پیچیده‌تر می‌شود از Adobe After Effects استفاده کرد. پس از ساخت انیمیشن مربوطه، با استفاده از Lottie انیمیشن به یک فایل JSON تبدیل می‌شود. دقت کنید که Animationهای After Effects به صورت فیلم و Gif نیستند، بلکه در آنها توصیف یک Animation ذخیره می‌شود که این یک نقطه قدرت After Effects است. در سایت Lottiefiles می توانید تعداد زیادی از این JSON‌های آماده را دانلود کنید.
برای شروع ابتدا نسخه 2.6.3 پکیج  Com.Airbnb.Xamarin.Forms.Lottie را روی پروژه نصب می‌کنیم. ما برای تست، از این JSON استفاده کرده‌ایم و آن را در مسیرهای زیر قرار داده‌ایم:
XamApp.Android/Assets/Animations/LottieLogo1.json 
XamApp.UWP/Assets/Animations/LottieLogo1.json 
XamApp.iOS/Assets/Animations/LottieLogo1.json 
البته همانند فونت‌ها، برای این که یک فایل را سه بار در سورس کنترلر کپی نکنیم، از روش Add as link استفاده کرده‌ایم.
سپس از کد زیر برای نمایش فایل مربوطه استفاده می‌کنیم:
<lottie:AnimationView
    Animation="{OnPlatform UWP='Assets/Animations/LottieLogo1.json',
                                   Android='Animations/LottieLogo1.json',
                                   iOS='Animations/LottieLogo1.json'}" 
    AutoPlay="True"
    HeightRequest="500"
    HorizontalOptions="Center"
    Loop="True"
    VerticalOptions="Center"
    WidthRequest="500" />
برای بررسی بهتر مثال‌ها، آخرین وضعیت XamApp را Clone/Pull کنید و در App.xaml.cs درخواست کنید که صفحه Animations را ببینید. کدها نیز در فایل AnimationsView.xaml هستند.
اشتراک‌ها
مشکل امنیتی رفع شده در Roxy FileManager

RoxyFileManager

Roxy FileManager یک فایل آپلود کارآمد بسیار کم حجم و رایگان می‌باشد که Php و Net. را پشتیبانی می‌کند اما مشکل امنیتی داشت که در نسخه 1.4.5 رفع گردیده و می‌توانید هم اکنون از سایت www.roxyfileman.com  آن دریافت کنید. 

اما این مشکل به چه صورت بود:
در داخل پوشه این ابزار فایل conf.json وجود دارد که اگر آن را با Notepad باز کنید تنظیمات پیش فرض این ابزار را مشاهده می‌کنید که در خط 28 موارد ممنوعه برای آپلود به عنوان FORBIDDEN_UPLOADS ذکر شده و از آپلود فایل‌های خطرناکی که در روش هک Shell Upload مورد استفاده قرار می‌گیرند جلوگیری می‌کند. تا اینجا همچی عالیه D:  ! 

اما در Roxy File Manager امکان دیگری وجود دارد و آن تغییر نام فایل‌های آپلود شده می‌باشد که پسوند و نوع فایل در این حالت بررسی نمی‌شود و شما به راحتی می‌توانید فایل Shell خودتان را در هنگام آپلود تغییر نام دهید مثلاً  فایل شما Shell.php بوده و شما با تغییر نام آن به Shell.phpJPG می‌توانید آن را آپلود کنید سپس در محیط ابزار آن را تغییر نام دهید و نیت خیر خواهانه خودتان را به انجام برسانید.

از این رو اگر از Roxy File Manager نسخه پایین‌تر از 1.4.5 استفاده می‌کنید حتماً بروز رسانی نمائید !.
نحوه استفاده از این ابزار را می‌توانید در این پست  افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  مشاهده نمائید.

مشکل امنیتی رفع شده در Roxy FileManager
مطالب
بررسی نحوه‌ی راه اندازی پروژه‌ی Decision
پروژه‌ی Decision را می‌توان چکیده‌ی تمام مطالب سایت دانست که در آن جمع آوری نکات ASP.NET MVC 5.x، EF Code First 6.x، مباحث تزریق وابستگی‌ها، کار با AutoMapper، بوت استرپ 3 و غیره لحاظ شده‌اند. به همین جهت درک آن بدون مطالعه‌ی « تمام » مطالب سایت میسر نیست و همچنین راه اندازی آن.
در این مطلب با توجه به سؤالات زیادی که در مورد صرفا نحوه‌ی اجرای بدون خطای آن وجود داشت، ریز مراحل آن‌را بررسی می‌کنیم.


پیشنیازهای توسعه‌ی برنامه
- با توجه به استفاده از ویژگی‌های C# 6 در این پروژه، حتما نیاز است برای کار و اجرای آن از VS 2015 استفاده کنید.
- همچنین این پروژه از قابلیت «فایل استریم» SQL Server استفاده می‌کند. بنابراین نیاز است نگارش متناسبی از SQL Server را پیشتر نصب کرده باشید (هر نگارشی بالاتر از SQL Server 2005).
- اگر از ReSharper استفاده می‌کنید، به صورت موقت آن‌را به حالت تعلیق درآورید (منوی tools، گرینه‌ی options و انتخاب resharper و سپس suspend کردن آن). این مورد سرعت بازیابی بسته‌های نیوگت را به شدت افزایش می‌دهد.


بازیابی وابستگی‌های نیوگت پروژه

مرسوم نیست چند 10 مگابایت وابستگی‌های پروژه را به صورت فایل‌های باینری، به مخزن کدها ارسال کرد. از این جهت که نیوگت بر اساس مداخل فایل‌های packages.config، قابلیت بازیابی و نصب خودکار آن‌ها را دارد. بنابراین ابتدا package manger console را باز کنید؛ از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console


همانطور که در تصویر مشاهده می‌کنید، نیوگت تشخیص داده‌است که بسته‌هایی برای نصب وجود دارند. بنابراین بر روی دکمه‌ی restore کلیک کنید تا کار دریافت و نصب خودکار این بسته‌ها از اینترنت شروع شود. البته اگر پیشتر این بسته‌ها را در پروژه‌های دیگری نصب کرده باشید، نیوگت از کش موجود در سیستم استفاده خواهد کرد و برای دریافت آن‌ها به اینترنت مراجعه نمی‌کند. ولی در هر حال اتصال به اینترنت ضروری است.

پس از پایان کار بازیابی بسته‌ها، یکبار کل Solution را Build کنید تا مطمئن شوید که تمام بسته‌های مورد نیاز به درستی بازیابی و نصب شده‌اند (Ctrl+Shift+B و یا همان منوی Build و انتخاب گزینه‌ی Build Solution).



تنظیمات رشته اتصالی بانک اطلاعاتی برنامه

پس از Build موفق کل Solution در مرحله‌ی قبل، اکنون نوبت به برپایی تنظیمات بانک اطلاعاتی برنامه است. برای این منظور فایل web.config ذیل را باز کنید:
Decision\src\Decision.Web\Web.config
یک چنین تنظیمی را مشاهده می‌کنید:
  <connectionStrings>
    <clear />
    <add name="DefaultConnection" connectionString="Data Source=.\sqlexpress;Initial Catalog=DecisionDb;Integrated Security = true;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
  </connectionStrings>
از آنجائیکه بر روی سیستم من SQL Server نگارش Developer نصب است و از SQL Server Express استفاده نمی‌کنم، تنظیمات فوق را به نحو ذیل تغییر خواهم داد:
  <connectionStrings>
    <clear />
    <add name="DefaultConnection" connectionString="Data Source=(local);Initial Catalog=DecisionDb;Integrated Security = true;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" />
  </connectionStrings>
تنها تغییر صورت گرفته، تنظیم data source است. مابقی موارد یکی است و تفاوتی نمی‌کند.

در این حالت نیاز است بانک اطلاعاتی خالی DecisionDb را خودتان ایجاد کنید. علت آن به AutomaticMigrationsEnabled = false بر می‌گردد؛ که در ادامه توضیح داده شده‌است و همچنین وجود تنظیم ذیل در فایل Decision\src\Decision.Web\App_Start\ApplicationStart.cs
 Database.SetInitializer<ApplicationDbContext>(null);
این تنظیم و نال بودن پارامتر ورودی آن به این معنا است که اولا برنامه یک بانک اطلاعاتی جدید را به صورت خودکار ایجاد نمی‌کند و همچنین کار Migrations خودکار نیست.


ایجاد بانک اطلاعاتی برنامه و تنظیمات آن

پس از آن، نوبت به ایجاد بانک اطلاعاتی برنامه است. چون این برنامه از EF Code first استفاده می‌کند، قادر است بانک اطلاعاتی ذکر شده‌ی در Initial Catalog فوق را به صورت خودکار ایجاد کند (با تمام جداول، روابط و تنظیمات آن‌ها). این اطلاعات هم از پروژه‌ی Decision.DataLayer و پوشه‌ی Migrations آن تامین می‌شوند.
اگر به فایل Decision\src\Decision.DataLayer\Migrations\201602072159421_Initial.cs مراجعه کنید، یکسری تنظیمات دستی را هم علاوه بر کدهای خودکار EF، مشاهده خواهید کرد:
 //. . .
Sql("EXEC sp_configure filestream_access_level, 2");
Sql("RECONFIGURE", true);

Sql("alter database DecisionDb Add FileGroup FileGroupApplicant contains FileStream", true);
Sql("alter database DecisionDb add file ( name = 'ApplicantDocuements'  ,  filename = 'C:\\FileStream\\ApplicantDocuements') to filegroup FileGroupApplicant", true);
//. . .
این‌ها مواردی هستند که کار تنظیمات فایل استریم را به صورت خودکار انجام می‌دهند.
بنابراین نیاز است در درایور C، پوشه‌ی خالی FileStream از پیش تهیه شده باشد (نیازی به ایجاد پوشه‌ی ApplicantDocuements نیست و این پوشه به صورت خودکار ایجاد می‌شود).

و در فایل Decision\src\Decision.DataLayer\Migrations\Configuration.cs مشخص شده‌است که AutomaticMigrationsEnabled = false. به این معنا که تنظیمات فوق به صورت خودکار به بانک اطلاعاتی اعمال نشده و باید چند دستور ذیل را به صورت دستی صادر کنیم:
الف) ابتدا package manager console را مجددا باز کنید و در اینجا default project را بر روی Decision.DataLayer قرار دهید. از این جهت که قرار است اطلاعات migration را از این پروژه دریافت کنیم:


در غیراینصورت پیام خطای No migrations configuration type was found in the assembly را دریافت خواهید کرد.

ب) سپس دستور ذیل را صادر کنید (با این فرض که بانک اطلاعاتی خالی DecisionDb ذکر شده‌ی در قسمت قبل را پیشتر ایجاد کرده‌اید):
 PM> Update-Database -Verbose -ConnectionStringName "DefaultConnection" -StartUpProjectName "Decision.Web"
این تنظیمات به این معنا است که Update-Database را بر اساس اطلاعات پروژه‌ی Decision.DataLayer انجام بده (همان انتخاب default project)؛ اما رشته‌ی اتصالی را از پروژه‌ی Decision.Web و تنظیمات DefaultConnection آن دریافت کن.

من در این حالت پیام خطای Update-Database : The term 'Update-Database' is not recognized as the name of a cmdlet را دریافت کردم.
راه حل: یکبار ویژوال استودیو را بسته و مجددا باز کنید تا کار نصب بسته‌ها و بارگذاری تمام وابستگی‌های آن‌ها به درستی صورت گیرد. این خطا به این معنا است که هرچند NuGet کار نصب EF را انجام داده‌است، اما هنوز اسکریپت‌های پاورشل آن که دستوراتی مانند Update-Database را اجرا می‌کنند، بارگذاری نشده‌اند. راه حل آن بستن و اجرای مجدد ویژوال استودیو است.
پس از اجرای مجدد ویژوال استودیو و انتخاب default project صحیح (مطابق تصویر فوق)، مجددا دستور Update-Database  فوق را صادر کنید (با پارامترهای ویژه‌ی آن).
با صدور این دستور، پیام خطای ذیل را دریافت کردم:
 The Entity Framework provider type 'System.Data.Entity.SqlServer.SqlProviderServices, EntityFramework'
registered in the application config file for the ADO.NET provider with invariant name 'System.Data.SqlClient' could not be loaded.
برای رفع آن نیاز است EF را یکبار دیگر نصب کنید:
 PM> Update-Package -Reinstall "EntityFramework" -ProjectName "Decision.DataLayer"
در ادامه مجددا کل Solution را Build کنید؛ چون Migrations بر اساس اطلاعات اسمبلی‌های کامپایل شده‌ی پروژه کار می‌کند.
اینبار دستور update-database فوق (با پارامترهای ویژه‌ی آن) بدون مشکل اجرا شد و بانک اطلاعاتی مربوطه تشکیل گردید.




اکنون برنامه قابل اجرا است و در این حالت است که می‌توان دکمه‌ی F5 را جهت اجرای برنامه فشرد. البته در این حالت بر روی پروژه‌ی Decision.Web کلیک راست کرده و گزینه‌ی set as startup project را نیز انتخاب کنید و سپس F5:



لطفا سؤالاتی را که مرتبط با «راه اندازی» این پروژه نیستند، در قسمت بازخوردهای اختصاصی آن مطرح کنید.
مطالب
مروری بر کتابخانه ReactJS - قسمت هشتم - آخرین قسمت - چرخه حیات کامپوننت‌ها

هر کامپوننتی در React یک چرخه زندگی دارد. زمانیکه یک کامپوننت را به روش React.createClass یا React.Component تعریف میکنیم و در ReactDOM.render نمونه‌ای از کامپوننت را برای نمایش در مرورگر می‌سازیم، چرخه حیات آن شروع میشود. 


ReactDOMServer

کتابخانه ReactDOMServer جهت ساخت یا render کردن کامپوننت‌ها در سمت سرور استفاده میشود. توسط این کتابخانه میتوانیم کامپوننت‌ها را در سمت سرور ایجاد کنیم و نتیجه آن را که تگ‌های HTML هستند به مرورگر ارسال کنیم. این روش جهت داشتن صفحه‌های وب سریع‌تر و اهداف SEO مفید است. جهت اطلاعات بیشتر و روش‌های استفاده به مستندات آن رجوع کنید. در مثال زیر روش استفاده از این کتابخانه به اختصار آمده.

var persons = [
    { id: 1, personName: "Parham", personContact: "parhamda@gmail.com" },
    { id: 2, personName: "Roham", personContact: "roham@yahoo.com" },
    { id: 3, personName: "Raha", personContact: "raha@live.com" }
];

class Person extends React.Component{
    render(){
        return (
            <div>
                <p>{this.props.personName}</p>
                <p>{this.props.personContact}</p>
            </div>
        )
    }
}

let person1 = persons[0];
let personElement = <Person personName={person1.personName} personContact={person1.personContact}/>
console.log(ReactDOMServer.renderToStaticMarkup(personElement));

در کد بالا مواردی که جدید هستند، یکی ساخت یک نمونه از کامپوننت Person است و دیگری ساخت آن در سمت سرور، بدون آن که فعلا نمایشی در مرورگر داشته باشیم. در کنسول میتوانیم خروجی کتابخانه را که تگ‌های HTML هستند ببینیم. ReactDOMServer دو متد را فراهم کرده که کارکردی مشابه دارند؛ اما در جزئیات متفاوت هستند. 

  • renderStaticMarkup یک خروجی استاتیک و بدون attributeهای اضافه را تولید میکند که بیشتر برای بررسی یا استفاده در صفحه‌های وب ایستا مفید هستند.
  • renderToString یک خروجی به صورت HTML String ایجاد میکند که برای HTML DOM در سمت کاربر سازگار‌تر است و مناسب برای صفحات پویا. 

در نهایت خروجی از هر نوع که بود، برای اینکه در سمت کاربر قابل مشاهده باشد باید از همان متد ReactDOM.render استفاده کنیم. از آنجایی که این مجموعه جهت معرفی و بررسی ابزارهای اصلی React به صورت مختصر است، از آوردن مثال‌های زیاد و پیچیده پرهیز میکنم. در اینجا میتوانید یک نمونه ساده برای استفاده از ReactDOMServer به صورت استاندارد و با جزئیات را بررسی کنید.


متدهای چرخه حیات در React

React چند متد را برای زمان‌های قبل و بعد از ساخت شدن یک کامپوننت در DOM دارد که میشود رفتارهایی را برای کامپوننت، در این متدها در نظر گرفت تا در زمان مناسب اجرا شوند. در ادامه این متد‌ها معرفی و کاربرد هر یک بیان میشود. 


 componentWillMount: این متد قبل از اینکه کامپوننت، تگ‌های متد render را بسازد اجرا میشود. این متد هم در سمت کلاینت کاربرد دارد و هم در سمت سرور. به همین جهت برای گرفتن log از داده‌های کامپوننت و کار با پایگاه داده مکان مناسبی است. به عنوان مثال در قطعه کد زیر داده‌های کامپوننت، توسط Ajax ارسال شده‌اند. 

componentWillMount() {
   Ajax.post("/componentLog", {
     name: this.constructor.name,
     props: this.props
   });
}

componentDidMount: این متد بعد از اینکه بخش render اجرا شد فراخوانی میشود. همچنین فقط در سمت کلاینت و زمانیکه از ReactDOM.render استفاده میکنیم کاربرد دارد. این متد مناسب برای تعامل کامپوننت با افزونه‌ها و API‌ها است؛ مانند دریافت اطلاعات مورد نیاز کامپوننت از سایتی دیگر توسط یک API. از  این متد در قسمت چهارم مثالی آورده شده. 


(componentWillReciveProps(nextProps: این متد زمانی اجرا میشود که داده‌های ورودی کامپوننت با مقادیری جدید تغییر کنند.

componentWillReceiveProps(nextProps) {
    // Do something with new received data and change the state. 
}

ReactDOM.render(
    <TestComponent someData={newDataEveryFiveSecond()}/>,
    document.getElementById("divTest")
);

در مثال بالا یک کامپوننت داریم که داده‌های ورودی خود را از یک تابع میگیرد. این تابع هر پنج ثانیه یک بار یک داده تازه ایجاد میکند و به کامپوننت ارسال میکند. میتوانیم داخل کامپوننت، از متد componentWillReceiveProps جهت دستکاری داده‌های رسیده و تغییر وضعیت کامپوننت توسط setState استفاده کنیم. 


(shouldComponentUpdate(nextProps, nextState: این متد شبیه به متد componentWillReceiveProps است، البته با تفاوت‌هایی. این متد هم مقدار ورودی جدید برای پارامتر‌های کامپوننت میگیرد و هم مقداری برای وضعیتی که کامپوننت دارد. این متد باید یک مقدار بازگشتی false یا  true داشته باشد. با این مقدار بازگشتی میتوان کنترل کرد که آیا کامپوننت بر اساس داده‌های جدید بروز بشود یا نه. 

class ComponentExample extends React.Component {
    shouldComponentUpdate(nextProps, nextState) {
        return notEqual(this.props, nextProps) ||
            notEqual(this.state, nextState);
    }
}

در مثال بالا پارامترها و وضعیت جاری کامپوننت، با مقدارهای تازه تغییر یافته و وضعیت جدید مقایسه میشوند. اگر مقادیر مقایسه شده برابر نباشند (یعنی داده تکراری وارد نشده) مقدار بازگشتی true خواهد بود و React کامپوننت را بر اساس وضعیت جدید و داده‌های تازه دوباره میسازد.


(componentWillUpdate(nextProps, nextState: این متد زمانیکه کامپوننت ساخته شده، داده‌های جدیدی را دریافت کند و یا وضعیت آن تغییر کند و دقیقا قبل از اجرای render فراخوانی میشود. اگر از متد shouldComponentUpdate مقدار false بازگشت داده شود، این متد دیگر اجرا نخواهد شد. باید توجه داشته باشیم که setState را نمیشود در این متد پیاده‌سازی کرده. به این علت که، زمانیکه وضعیت کامپوننت تغییر میکند، React متد componentWillUpdate و بلافاصله بعد از آن render را اجرا میکند و برای تغییر وضعیت دیگر دیر شده! تفاوت componentWillUpdate با componentWillMount  این است که Will Mount در اولین وهله سازی از کامپوننت اجرا میشود، ولی Will Update بعد از هر دوباره سازی (rerender). 


(componentDidUpdate(prevProps, prevStat: احتمالا میشود به راحتی حدس زد که این متد دقیقا بعد از دوباره سازی کامپوننتی که ساخته شده فراخوانی میشود.


componentWillUnmount: این متد زمانی اجرا میشود که یک کامپوننت از DOM پاک شود. برای پاک کردن نمونه‌ای از یک کامپوننت که در DOM در حال نمایش است میتوانیم از دستور زیر استفاده کنیم. 

ReactDOM.unmountComponentAtNode(document.getElementById("react"));
بازخوردهای پروژه‌ها
نمونه برنامه نوشته شده با MVC
با تشکر از سری آموزشی بی نظیر MVC
بنده این سری آموزشی را کامل دنبال کرده و از آموزش روان مختصر و کاربردیتان بسیار لذت بردم.
اما خلایی که در این میان وجود دارد یک پروژه آموزشی نوشته شده با MVC که تمرکز آن بر روی رعایت الگو‌های رایج نرم افزار (مثل DI , Repository) معماری چند لایه و کد هایی ترو تمیز و refactor  شده و نحوه استفاده درست ViewModel‌ها و به طور خلاصه رعایت حداقل بخش مهم چک لیست تهیه برنامه‌های MVC  باشد
وقتی نظرات را در ذیل مطالبی مثل این  و این   می‌خونم احساس می‌کنم از نیاز به وجود همچین نمونه برنامه ای برای روشن شدن مطلب احساس می‌شود 
برای شروع هر پروژه نرم افزاری ابتدا نیاز به چهارچوب و یک سیم کشی اولیه است که بنده نیاز به یادگیری آن دارم پروژه هایی مثل این را هم دیده ام که باز هم به طور کامل نیاز من را برطرف نکرد
با تشکر
مطالب
بررسی تغییرات Blazor 8x - قسمت یازدهم - قالب جدید پیاده سازی اعتبارسنجی و احراز هویت - بخش اول
قالب‌های پیش‌فرض Blazor 8x، به همراه قسمت بازنویسی شده‌ی ASP.NET Core Identity برای Blazor هم هستند که در این قسمت به بررسی نحوه‌ی عملکرد آن‌ها می‌پردازیم.


معرفی قالب‌های جدید شروع پروژه‌های 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!;
متد ()AddRazorComponents ای که در فایل Program.cs اضافه می‌شود، کار ثبت CascadingParameter ویژه‌ی فوق را به صورت زیر انجام می‌دهد که در Blazor 8x به آن یک Root-level cascading value می‌گویند:
services.TryAddCascadingValue(sp => sp.GetRequiredService<EndpointHtmlRenderer>().HttpContext);
مقادیر آبشاری برای ارسال اطلاعاتی بین درختی از کامپوننت‌ها، از یک والد به چندین فرزند که چندین لایه ذیل آن واقع شده‌‌اند، مفید است. برای مثال فرض کنید می‌خواهید اطلاعات عمومی تنظیمات یک کاربر را به چندین زیر کامپوننت، ارسال کنید. یک روش آن، ارسال شیء آن به صورت پارامتر، به تک تک آن‌ها است و راه دیگر، تعریف یک CascadingParameter است؛ شبیه به کاری که در اینجا انجام شده‌است تا دیگر نیازی نباشد تا تک تک زیر کامپوننت‌ها این شیء را به یک لایه زیریرین خود، یکی یکی منتقل کنند.

در کدهای 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>
در نگارش‌های قبلی، این مسیریاب داخل کامپوننت CascadingAuthenticationState محصور می‌شد تا توسط آن بتوان AuthenticationState را به تمام کامپوننت‌های برنامه ارسال کرد. به این ترتیب برای مثال کامپوننت AuthorizeView، شروع به کار می‌کند و یا توسط شیء context.User می‌توان به User claims دسترسی یافت و یا به کمک ویژگی [Authorize]، دسترسی به صفحه‌ای را محدود به کاربران اعتبارسنجی شده کرد.
اما ... در اینجا با یک نگارش ساده شده سروکار داریم؛ از این جهت که برنامه‌های جدید، به همراه جزایر تعاملی هم می‌توانند باشند و باید بتوان این 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" });
پس از این تغییر، اکنون نه فقط صفحات SSR، بلکه جزایر واقع در آن‌ها نیز توسط ویژگی [CascadingParameter] می‌توانند به این مقدار آبشاری، دسترسی داشته باشند. به این ترتیب است که در برنامه‌های Blazor، کامپوننت‌های تعاملی هم دسترسی به اطلاعات شخص لاگین شده‌ی توسط صفحات SSR را دارند. برای مثال اگر به فایل Program.cs قالب جدید Identity UI عنوان شده مراجعه کنید، چنین سطوری در آن قابل مشاهده هستند
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
متد جدید AddCascadingAuthenticationState، فقط کار ثبت AuthenticationStateProvider برنامه را به صورت آبشاری انجام می‌دهد.
این AuthenticationStateProvider سفارشی جدید هم در فایل اختصاصی IdentityRevalidatingAuthenticationStateProvider.cs پوشه‌ی Identity قالب پروژه‌های جدید Blazor 8x که به همراه اعتبارسنجی هستند، قابل مشاهده‌است.

یا اگر به قالب‌های پروژه‌های WASM دار مراجعه کنید، تعریف به این صورت تغییر کرده‌است؛ اما مفهوم آن یکی است:
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddScoped<AuthenticationStateProvider, PersistingServerAuthenticationStateProvider>();
در این پروژه‌ها، یک AuthenticationStateProvider سفارشی دیگری در فایل PersistingServerAuthenticationStateProvider.cs ارائه شده‌است (این فایل‌ها، جزو استاندارد قالب‌های جدید Identity UI پروژه‌های Blazor 8x هستند؛ فقط کافی است، یک نمونه پروژه‌ی آزمایشی را با سوئیچ جدید auth Individual-- ایجاد کنید، تا بتوانید این فایل‌های یاد شده را مشاهده نمائید).

AuthenticationStateProviderهای سفارشی مانند کلاس‌های IdentityRevalidatingAuthenticationStateProvider و PersistingServerAuthenticationStateProvider که در این قالب‌ها موجود هستند، چون به صورت آبشاری کار تامین AuthenticationState را انجام می‌دهند، به این ترتیب می‌توان به شیء context.User.Identity@ در جزایر تعاملی نیز به سادگی دسترسی داشت.

یعنی به صورت خلاصه، یکبار قرارداد AuthenticationStateProvider عمومی (بدون هیچ نوع پیاده سازی) به صورت یک Root-level cascading value ثبت می‌شود تا در کل برنامه قابل دسترسی شود. سپس یک پیاده سازی اختصاصی از آن توسط ()<builder.Services.AddScoped<AuthenticationStateProvider, XyzProvider به برنامه معرفی می‌شود تا آن‌را تامین کند. به این ترتیب زیر ساخت امن سازی صفحات با استفاده از ویژگی attribute [Authorize]@ و یا دسترسی به اطلاعات کاربر جاری با استفاده از شیء context.User@ و یا امکان استفاده از کامپوننت AuthorizeView برای نمایش اطلاعاتی ویژه به کاربران اعتبارسنجی شده مانند صفحه‌ی Auth.razor زیر، مهیا می‌شود:
@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 و «فیلدهای مخفی» مرتبط با آن، بررسی می‌کنیم.
مطالب
بازسازی کد: جایگزینی آرایه با شیء (Replace array with object)
از آرایه برای ذخیره سازی آیتم‌های مشابه استفاده می‌شود. این تشابه باید علاوه بر اینکه در نوع داده‌ای آیتم‌ها رعایت شود، باید از نظر مفهومی نیز رعایت شود.
زمانیکه از یک آرایه برای نگهداری المنت‌های غیر مشابه استفاده می‌شود، نیاز به چنین بازسازی کدی است. به طور مثال آرایه‌ای که آیتم اول آن "نام" و آیتم دوم آن "امتیاز" است. قطعا کار با چنین آرایه‌ای بسیار مشکل خواهد بود. زمانیکه یک آرایه را از نوع داده‌ای عمومی‌تری (مثلا object در سی شارپ) تعریف و انواع داده‌ای متفاوت را در آیتم‌های آن نگهداری کنیم، اوضاع بسیار بدتر خواهد شد. 
محور اصلی بازسازی کد "جایگزینی آرایه با شیء" ایجاد یک کلاس، برای ذخیره اطلاعات آرایه است. به این صورت که برای هر آیتم آرایه، یک خصوصیت در کلاس مربوطه ایجاد می‌شود. 
به طور مثال به آرایه زیر توجه نمایید:
var row = new string[3]; 
row[0] = "Liverpool"; 
row[0] = "15";
در آرایه بالا، آیتم اول نشان دهنده نام تیم و آیتم دوم نشان دهنده امتیاز تیم است. با وجود اینکه این مثال کمی غیر واقعی به نظر میرسد، اما چنین مثال‌هایی در برنامه نویسی روزمره ممکن است به اشکال مختلفی مشاهده شود. مانند استفاده از dictionary برای دریافت اطلاعات فرم وب، استفاده از Tuple (در زبان سی شارپ) برای انتقال اطلاعات و … 
در این مثال طراحی بهتر، ایجاد یک کلاس یا ساختار (بسته به شرایط کلی مسئله) برای نشان دادن امتیاز تیم است:  
public class Performance 
{ 
       public string TeamName { get; set; } 
       public int Score { get; set; } 
}
همانطور که مشاهده می‌کنید، به ازای هر یک از آیتم‌های آرایه، خصوصیتی در کلاس جدید ایجاد شده‌است. همچنین انتخاب انواع داده‌ای نیز در طراحی جدید، ساده‌تر و اصولی‌تر انجام خواهد شد.
تمامی استفاده‌ها از آرایه‌ها، در دسته بندی این نوشتار برای بازسازی کد قرار نمی‌گیرند. آرایه‌هایی که اصل مشابه بودن آیتم‌ها را رعایت می‌کنند، معمولا نیازی به بازسازی کد ندارند. به طور مثال در نرم افزارهای فروشگاه اینترنتی، خصوصیات کالا به صورت داینامیک ذخیره شده و احتمالا برای دسترسی و مدیریت آن، از آرایه یا لیست استفاده می‌شود. اما با کمی دقت خواهیم دید، این استفاده از آرایه، با تعریف مشابه بودن آیتم‌ها همخوانی دارد. زیرا تمامی آیتم‌های آرایه به طور مثال از نوع خصوصیت کالا هستند. همچنین عملا امکان بازسازی و ایجاد کلاس در این مثال وجود ندارد؛ زیرا خصوصیات کالاها در زمان توسعه مشخص نیستند و در زمان اجرای برنامه تنظیم می‌شوند. 
مطالب
توسعه سرویس‌های مبتنی بر REST در AngularJs با استفاده از RestAngular : بخش دوم
در بخش پیشین  کلیات کتابخانه‌ی Restangular  را بررسی کردیم. در این بخش قصد داریم تا در طی یک پروژه، امکانات و قابلیت‌های بی‌نظیر این سرویس را در یک پروژه‌ی واقعی مشاهده کنیم.

کلیات پروژه

در این پروژه قصد داریم تا لیست کتاب‌های یک کتابخانه را نمایش دهیم. این کتابها قابلیت ویرایش نام دارند و همچنین شما می‌توانید کتابهای جدیدی را به لیست کتابها اضافه نمایید. تصویر زیر خروجی این پروژه است:

پایگاه داده‌ی برنامه با نام LibDb درون پوشه‌ی app_data قرار داده شده‌است. این پایگاه داده تنها دارای یک جدول است؛ با نام Books که دیاگرام آن را در شکل زیر مشاهده می‌کنید:


پیاده سازی

در ابتدا یک پروژه‌ی Empty را با رفرنس web API ایجاد می‌کنیم. حال درون فایل WebApiConfig.cs تکه کد زیر را اضافه می‌کنیم. این تکه کد فیلدها و آبجکت‌هایی را که از سمت سرور بازگشت داده می‌شوند، به صورت camelCase تبدیل می‌کند.
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();

jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
در ادامه مدل edmx را ساخته و پس از آن Books Web Api را درون پوشه‌ی کنترلر ایجاد می‌کنیم. این کنترلر به صورت Default web Api Scaffold ساخته شده است و به دلیل اینکه به بحث ما مرتبط نیست؛ از توضیح بیشتر آن می‌گذریم.
حال پوشه‌ای را با نام app برای نگه داری فایل‌های AngularJs اضافه می‌کنیم و درون آن فایل app.js را ایجاد می‌کنیم:
var angularExample = angular.module('angularexample', ["restangular"])
angularExample.config(["RestangularProvider", function (RestangularProvider) {
    //RestangularProvider.setRestangularFields({
    //    id: "id"
    //});
    RestangularProvider.setBaseUrl('/api');
}]);

angularExample.controller("MainCtrl", ["Restangular", "$scope", function (Restangular, $scope) {
    var resource = Restangular.all('books');
    resource.getList().then(function (response) {
        $scope.books = response;
    });
    $scope.add = function () {
        resource.post($scope.newBook).then(function (newResource) {
            $scope.books.push(newResource);
        })
    }
}]);
محتویات فایل app.js را مشاهده می‌کنید. در ادامه درباره‌ی این قسمت بیشتر صحبت می‌کنیم.
حال در روت پروژه فایل index.html را ایجاد می‌کنیم:
<!DOCTYPE html>
<html ng-app="angularexample" xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Rest Angular Sample</title>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
    <script src="http://cdn.jsdelivr.net/underscorejs/1.5.1/underscore-min.js"></script>
    <script src="/Scripts/restangular.min.js"></script>
    <script src="/app/app.js"></script>
</head>
<body>
    <div ng-controller="MainCtrl">
        <div ng-repeat="item in books">
            name is: {{item.name}}<br />
            change name: <input type="text" ng-model="item.name" /><button type="submit" ng-click="item.put();">update</button>
        </div>
        <div>
            add new: <br />
            <input type="text" ng-model="newBook.name" /><button type="submit" ng-click="add()">add</button>
        </div>
    </div>
</body>
و در نهایت در پوشه‌ی Scripts فایل‌های سرویس Restangular را قرار می‌دهیم.
تا به اینجای کار تمامی کارهای مورد نیاز تشکیل پروژه را انجام داده‌ایم. حال به بررسی بیشتر سرویس‌های این پروژه می‌پردازیم؛ یعنی کدهای درون فایل app.js.
ببینیم که برای دریافت لیست تمامی کتابها، ما چه کارهایی را انجام داده‌ایم! به تکه کد زیر دقت کنید:
    var resource = Restangular.all('books');
    resource.getList().then(function (response) {
        $scope.books = response;
    });
این تمامی آن چیزی است که ما برای دریافت لیست تمامی کتابها انجام داده‌ایم. به همین سادگی! در خط اول از متد all که در بخش قبل توضیح مختصری راجع به آن داده بودم برای دریافت لیست تمامی کتابها استفاده کردم که پارامتر درون آن آدرس Web Api است. البته همانطور که می‌دانید در ASP.NET Web Api ما همیشه به base address یک api نیز اضافه می‌کنیم. حال اینکه چرا api در اینجا نیامده در ادامه و در بخش تنظیمات کلی Restangular توضیح می‌دهم. در خط دوم نیز از اشاره‌گر resources متد getList را فراخوانی کرده و لیست کتابها را در response دریافت می‌کنیم.
پس از آن می‌خواهیم ببینیم که عملیات ایجاد یک کتاب جدید چگونه انجام می‌گردد. تکه کد زیر این عملیات را انجام می‌دهد:
    $scope.add = function () {
        resource.post($scope.newBook).then(function (newResource) {
            $scope.books.push(newResource);
        })
    }
ما یک متد با نام add ایجاد کرده‌ایم که در سمت View توسط دایرکتیو ng-click آن را فراخوانی می‌کنیم.
همانطور که مشاهده میکنید درون app.js متدی برای update موارد قبلی نیست. بیایید سری به View بزنیم. به المنت div زیر توجه کنید. همانطور که می‌بینید تمامی عملیات update با یک دستور ساده item.put حل و فصل شده.
        <div ng-repeat="item in books">
            name is: {{item.name}}<br />
            change name: <input type="text" ng-model="item.name" /><button type="submit" ng-click="item.put();">update</button>
        </div>
تمامی آنچه که گفته شد تنها بخشی از قابلیت‌های شگفت انگیز این افزونه بود. امیدوارم که مطلب مفید واقع شده باشد. سورس این پروژه را می‌توانید از لینک زیر دریافت کنید.

سورس پروژه: RestangularSample.rar  

مطالب دوره‌ها
پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC
مدتی است در اکثر سایت‌ها و طراحی‌های جدید، به جای استفاده از روش متداول نمایش انتخاب صفحه 1، 2 ... 100، برای صفحه بندی اطلاعات، از روش اسکرول نامحدود یا infinite scroll استفاده می‌کنند. نمونه‌ای از آن‌را هم در سایت جاری با دکمه «بیشتر» در ذیل اکثر صفحات و مطالب سایت مشاهده می‌کنید.


در ادامه قصد داریم نحوه پیاده سازی آن‌را در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.


مدل برنامه

namespace jQueryMvcSample02.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
در این برنامه و مثال، قصد داریم لیستی از مطالب را توسط اسکرول نامحدود، نمایش دهیم. هر آیتم نمایش داده شده، ساختاری همانند کلاس BlogPost دارد.


منبع داده فرضی برنامه

using System.Collections.Generic;
using System.Linq;
using jQueryMvcSample02.Models;

namespace jQueryMvcSample02.DataSource
{
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
برای اینکه برنامه نهایی را به سادگی بتوانید اجرا کنید، به عمد از بانک اطلاعاتی خاصی استفاده نشده و صرفا یک منبع داده فرضی تشکیل شده در حافظه، در اینجا مورد استفاده قرار گرفته است. بدیهی است قسمت cachedItems را به سادگی می‌توانید با یک ORM جایگزین کنید.
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts می‌باشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازه‌ای از اطلاعات که قرار است نمایش داده شوند، دریافت می‌گردد. خوشبختانه این متدها معادل‌های مناسبی را در اکثر بانک‌های اطلاعاتی داشته و استفاده از آن‌ها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار می‌کند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.


کنترلر برنامه

using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample02.DataSource;
using jQueryMvcSample02.Security;

namespace jQueryMvcSample02.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            //آغاز کار با صفحه صفر است
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(list); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult PagedIndex(int? page)
        {
            var pageNumber = page ?? 0;
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber);
            if (list == null || !list.Any())
                return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها

            return PartialView("_ItemsList", list);
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
کنترلر برنامه را در اینجا ملاحظه می‌کنید. برای کار با اسکرول نامحدود، به ازای هر صفحه، نیاز به دو متد است:
الف) یک متد که بر اساس HttpGet کار می‌کند. این متد در اولین بار نمایش صفحه فراخوانی می‌گردد و اطلاعات صفحه آغازین را نمایش می‌دهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستی‌های AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیک‌های کاربر بر روی دکمه «بیشتر» استفاده می‌گردد. بنابراین تنها کاری که افزونه جی‌کوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده می‌شود.


دو View برنامه

همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر می‌کند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <h5>
                @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new  { id = item.Id },
                                 htmlAttributes: null)
            </h5>
            @item.Body
        </li>
    }
</ul>
ب) یک View کامل که در بار اول نمایش صفحه، مورد استفاده قرار می‌گیرد:
@model IList<jQueryMvcSample02.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home");
}
<h2>
    اسکرول نامحدود</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
<div id="MoreInfoDiv">
</div>
<div align="center" style="margin-bottom: 9px;">
    <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span>
</div>
<div id="ProgressDiv" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#moreInfoButton").InfiniteScroll({
                moreInfoDiv: '#MoreInfoDiv',
                progressDiv: '#ProgressDiv',
                loadInfoUrl: '@loadInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد
                },
                noMoreInfoHandler: function () {
                    alert('اطلاعات بیشتری یافت نشد');
                }
            });
        });
    </script>
}
چند نکته در اینجا حائز اهمیت است:
1) مسیر دقیق اکشن متد PagedIndex توسط متد  Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده می‌کنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایل‌های آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک می‌کند، توسط افزونه جی‌کوئری ما نمایان شده و در پایان کار مجددا مخفی می‌گردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام می‌دهد.


و کدهای افزونه اسکرول نامحدود

// <![CDATA[
(function ($) {
    $.fn.InfiniteScroll = function (options) {
        var defaults = {
            moreInfoDiv: '#MoreInfoDiv',
            progressDiv: '#Progress',
            loadInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            noMoreInfoHandler: null
        };
        var options = $.extend(defaults, options);

        var showProgress = function () {
            $(options.progressDiv).css("display", "block");
        }

        var hideProgress = function () {
            $(options.progressDiv).css("display", "none");
        }       

        return this.each(function () {
            var moreInfoButton = $(this);
            var page = 1;
            $(moreInfoButton).click(function (event) {
                showProgress();
                $.ajax({
                    type: "POST",
                    url: options.loadInfoUrl,
                    data: JSON.stringify({ page: page }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else {
                            if (data == "no-more-info") {
                                if (options.noMoreInfoHandler)
                                    options.noMoreInfoHandler(this);
                            }
                            else {
                                var $boxes = $(data);
                                $(options.moreInfoDiv).append($boxes);
                            }
                            page++;
                        }
                        hideProgress();
                        if (options.completeHandler)
                            options.completeHandler(this);
                    }
                });
            });
        });
    };
})(jQuery);
// ]]>
ساختار افزونه اسکرول نامحدود به این شرح است:
هربار که کاربر بر روی دکمه بیشتر کلیک می‌کند، progress div ظاهر می‌گردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال می‌گردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه می‌کند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی می‌کند.

دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip
 
مطالب
لینک‌های هفته دوم دی

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)


ASP. Net


طراحی و توسعه وب


PHP


اس‌کیوال سرور


سی شارپ


SharePoint

عمومی دات نت


ویندوز


مسایل اجتماعی و انسانی برنامه نویسی


متفرقه