رفتار Blazorهای پیش از داتنت 8 در مورد مدیریت حالت
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
Auto Render Mode، آخرین حالت رندری است که به Blazor 8x اضافه شدهاست. اگر از Blazor Server استفاده کنیم، به یک آغاز سریع در برنامه خواهیم رسید، به همراه مقداری تاخیر جزئی، برای به روز رسانی UI؛ از این جهت که تعاملات صورت گرفته باید از طریق اتصال وبسوکت SignalR به سرور ارسال شده و منتظر نتیجهی نهایی، برای اعمال آن به صفحه شد و یا باید به مقیاس پذیری این اتصالات همزمان با تعداد کاربران بالا هم اندیشید. اگر از Blazor WASM استفاده کنیم، آغاز آن، اندکی کند خواهد بود تا فایلهای فریمورک و برنامه، به درون مرورگر کاربر منتقل شوند. اما پس از آن همهچیز بسیار سریع است؛ از این جهت که تعاملات با DOM، توسط مرورگر و در همان سمت کاربر مدیریت میشود.
اما ... چقدر خوب میشد که امکان ترکیب هردوی اینها با هم در یک برنامه وجود میداشت؛ یعنی داشتن یک آغاز سریع، به همراه تعاملات سریع با DOM. به همین جهت Auto Render Mode به Blazor 8x اضافه شدهاست.
نحوهی عملکرد حالت رندر تعاملی خودکار در Blazor 8x
زمانیکه از قرار است از Auto Render Mode استفاده شود، یعنی در نهایت به سراغ حالت رندر وباسمبلی رفتن؛ اما به شرطیکه که فریمورک، مطمئن شود میتواند تمام فایلهای مرتبط را خیلی سریع و در کمتر از 100 میلیثانیه تامین کند که عموما یک چنین حالتی به معنای از پیش دریافت کردن این فایلها و کش شده بودن آنها در مرورگر است. اما اگر یک چنین تضمینی وجود نداشته باشد، از همان ابتدای کار تصمیم میگیرد که باید کامپوننت را از طریق نگارش Blazor Server آن ارائه دهد، تا آغاز سریعی را سبب شود. در این بین هم در پشت صحنه (یعنی زمانیکه کاربر مشغول به کار با نگارش Blazor Server کامپوننت است)، شروع به دریافت فایلهای مرتبط با نگارش وباسمبلی کامپوننت و برنامه میشود تا آنها را کش کرده و برای بار بعدی بارگذاری صفحه و نمایش اطلاعات آن، به سرعت از آنها استفاده کند.
یک چنین حالتی برای کاربران به این معنا است که به محض گشودن برنامه و صفحهای، قادر به استفادهی از آن هستند و برای بارهای بعدی استفاده، دیگر نیازی به اتصال دائم SignalR یک جزیرهی تعاملی Blazor Server نداشته و در نتیجه بار کمتری به سرور تحمیل خواهد شد (مقیاس پذیری بیشتر) و همچنین پردازش DOM بسیار سریعتری را نیز شاهد خواهند بود (کار با نگارش Blazor WASM درون مرورگر).
همانطور که در این تصویر هم مشخص است، برای بار اول نمایش یک چنین جزیرههایی، یک اتصال وبسوکت برقرار میشود که به معنای فعال شدن حالت جزیرهای Blazor Server است که در قسمت پنجم بررسی کردیم. در این بین فایلهای Blazor WASM این جزیره هم دریافت و کش میشوند که در کنسول توسعه دهندههای مرورگر، لاگ شدهاست. این اتصال وبسوکت، در بار اول نمایش این کامپوننت، بسته نخواهد شد؛ تا زمانیکه کاربر به صفحهای دیگر مراجعه کند. در دفعهی بعدی که درخواست نمایش این صفحه را داشته باشیم، چون اطلاعات نگارش وباسمبلی آن کش شدهاست، از همان ابتدای کار نگارش وب اسمبلی را بارگذاری و راهاندازی میکند.
تفاوت قالب پروژههای Auto Render Mode با سایر حالتهای رندر در Blazor 8x
برای ایجاد قالب ابتدایی پروژهی یک چنین حالت رندری، از دستور dotnet new blazor --interactivity Auto استفاده میشود که حالت تعاملی آن به Auto تنظیم شدهاست. در نگاه اول، Solution ایجاد شدهی آن، بسیار شبیه به Solution جزیرههای تعاملی Blazor WASM است که در قسمت هفتم به همراه یک مثال کامل بررسی کردیم؛ یعنی از دو پروژهی سمت سرور و سمت کلاینت تشکیل میشود و دارای این تفاوتها است:
در فایل Program.cs پروژهی سمت سرور آن، افزوده شدن هر دو حالت جزایر تعاملی Blazor Server و همچنین Blazor WASM را مشاهده میکنیم:
یک چنین قالبی میتواند تمام موارد زیر را با هم در یک Solution پشتیبانی کند:
الف) امکان تعریف صفحات فقط SSR در پروژهی سمت سرور
ب) امکان داشتن جزیرههای تعاملی فقط Blazor Server در پروژهی سمت سرور
ج) امکان داشتن جزیرههای تعاملی فقط Blazor Wasm در پروژهی سمت کلاینت
د) به همراه امکان تعریف جزیرهای تعاملی Auto Render Mode در پروژهی سمت کلاینت
یک نکته: در این تنظیمات، متد AddAdditionalAssemblies، امکان استفاده از کامپوننتهای قرار گرفتهی در سایر اسمبلیها و پروژهها را میسر میکند.
نحوهی تعریف کامپوننتهایی که قرار است توسط Auto Render Mode ارائه شوند
باتوجه به اینکه این نوع کامپوننتها در نهایت قرار است به صورت وباسمبلی رندر شوند، آنها را باید در پروژهی سمت کلاینت قرار داد و به نکات مرتبط با توسعهی آنها که در قسمت هفتم پرداختیم، توجه داشت.
همچنین مانند سایر حالتهای رندر، به دو طریق میتوان مشخص کرد که یک کامپوننت باید به چه صورتی رندر شود:
الف) استفاده از دایرکتیو حالت رندر با مقدار InteractiveAuto در ابتدای تعریف یک کامپوننت
ب) مشخص کردن حالت رندر، در زمان استفاده از المان کامپوننت
البته به شرطیکه using static زیر را به فایل Imports.razor_ پروژه اضافه کرد:
اما ... چقدر خوب میشد که امکان ترکیب هردوی اینها با هم در یک برنامه وجود میداشت؛ یعنی داشتن یک آغاز سریع، به همراه تعاملات سریع با DOM. به همین جهت Auto Render Mode به Blazor 8x اضافه شدهاست.
نحوهی عملکرد حالت رندر تعاملی خودکار در Blazor 8x
زمانیکه از قرار است از Auto Render Mode استفاده شود، یعنی در نهایت به سراغ حالت رندر وباسمبلی رفتن؛ اما به شرطیکه که فریمورک، مطمئن شود میتواند تمام فایلهای مرتبط را خیلی سریع و در کمتر از 100 میلیثانیه تامین کند که عموما یک چنین حالتی به معنای از پیش دریافت کردن این فایلها و کش شده بودن آنها در مرورگر است. اما اگر یک چنین تضمینی وجود نداشته باشد، از همان ابتدای کار تصمیم میگیرد که باید کامپوننت را از طریق نگارش Blazor Server آن ارائه دهد، تا آغاز سریعی را سبب شود. در این بین هم در پشت صحنه (یعنی زمانیکه کاربر مشغول به کار با نگارش Blazor Server کامپوننت است)، شروع به دریافت فایلهای مرتبط با نگارش وباسمبلی کامپوننت و برنامه میشود تا آنها را کش کرده و برای بار بعدی بارگذاری صفحه و نمایش اطلاعات آن، به سرعت از آنها استفاده کند.
یک چنین حالتی برای کاربران به این معنا است که به محض گشودن برنامه و صفحهای، قادر به استفادهی از آن هستند و برای بارهای بعدی استفاده، دیگر نیازی به اتصال دائم SignalR یک جزیرهی تعاملی Blazor Server نداشته و در نتیجه بار کمتری به سرور تحمیل خواهد شد (مقیاس پذیری بیشتر) و همچنین پردازش DOM بسیار سریعتری را نیز شاهد خواهند بود (کار با نگارش Blazor WASM درون مرورگر).
همانطور که در این تصویر هم مشخص است، برای بار اول نمایش یک چنین جزیرههایی، یک اتصال وبسوکت برقرار میشود که به معنای فعال شدن حالت جزیرهای Blazor Server است که در قسمت پنجم بررسی کردیم. در این بین فایلهای Blazor WASM این جزیره هم دریافت و کش میشوند که در کنسول توسعه دهندههای مرورگر، لاگ شدهاست. این اتصال وبسوکت، در بار اول نمایش این کامپوننت، بسته نخواهد شد؛ تا زمانیکه کاربر به صفحهای دیگر مراجعه کند. در دفعهی بعدی که درخواست نمایش این صفحه را داشته باشیم، چون اطلاعات نگارش وباسمبلی آن کش شدهاست، از همان ابتدای کار نگارش وب اسمبلی را بارگذاری و راهاندازی میکند.
تفاوت قالب پروژههای Auto Render Mode با سایر حالتهای رندر در Blazor 8x
برای ایجاد قالب ابتدایی پروژهی یک چنین حالت رندری، از دستور dotnet new blazor --interactivity Auto استفاده میشود که حالت تعاملی آن به Auto تنظیم شدهاست. در نگاه اول، Solution ایجاد شدهی آن، بسیار شبیه به Solution جزیرههای تعاملی Blazor WASM است که در قسمت هفتم به همراه یک مثال کامل بررسی کردیم؛ یعنی از دو پروژهی سمت سرور و سمت کلاینت تشکیل میشود و دارای این تفاوتها است:
در فایل Program.cs پروژهی سمت سرور آن، افزوده شدن هر دو حالت جزایر تعاملی Blazor Server و همچنین Blazor WASM را مشاهده میکنیم:
// ... builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); // ... app.MapRazorComponents<App>() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(Counter).Assembly);
الف) امکان تعریف صفحات فقط SSR در پروژهی سمت سرور
ب) امکان داشتن جزیرههای تعاملی فقط Blazor Server در پروژهی سمت سرور
ج) امکان داشتن جزیرههای تعاملی فقط Blazor Wasm در پروژهی سمت کلاینت
د) به همراه امکان تعریف جزیرهای تعاملی Auto Render Mode در پروژهی سمت کلاینت
یک نکته: در این تنظیمات، متد AddAdditionalAssemblies، امکان استفاده از کامپوننتهای قرار گرفتهی در سایر اسمبلیها و پروژهها را میسر میکند.
نحوهی تعریف کامپوننتهایی که قرار است توسط Auto Render Mode ارائه شوند
باتوجه به اینکه این نوع کامپوننتها در نهایت قرار است به صورت وباسمبلی رندر شوند، آنها را باید در پروژهی سمت کلاینت قرار داد و به نکات مرتبط با توسعهی آنها که در قسمت هفتم پرداختیم، توجه داشت.
همچنین مانند سایر حالتهای رندر، به دو طریق میتوان مشخص کرد که یک کامپوننت باید به چه صورتی رندر شود:
الف) استفاده از دایرکتیو حالت رندر با مقدار InteractiveAuto در ابتدای تعریف یک کامپوننت
@rendermode InteractiveAuto
<Banner @rendermode="@InteractiveAuto" Text="Hello"/>
@using static Microsoft.AspNetCore.Components.Web.RenderMode
اگر پیشتر با فناوریهای مرتبط با خانوادهی ASP.NET کار کرده باشید، با مفاهیمی مانند ContentPlaceHolder در وبفرمها و یا RenderSection در ASP.NET MVC، برخورد داشتهاید. دقیقا یک چنین قابلیتی نیز به Blazor 8x تحت عنوان Sections اضافه شدهاست تا توسط آن بتوان محتوای قسمتی از قالب کلی صفحه را از طریق زیر کامپوننتهای آن تغییر داد و کنترل کرد.
کامپوننتهای جدید SectionOutlet و SectionContent در Blazor 8x
پیاده سازی Sections در Blazor 8x به کمک دو کامپوننت جدید SectionOutlet و SectionContent میسر شدهاست و برای دسترسی به آنها نیاز است ابتدا به فایل Imports.razor_ پروژه، مراجعه کرد و using زیر را به آن اضافه نمود تا این اشیاء، در کامپوننتهای برنامه قابل شناسایی و استفاده شوند:
SectionOutlet کامپوننتی است که محتوای ارائه شدهی توسط کامپوننت SectionContent را رندر میکند (این محتوا در اصل یک RenderFragment است). ارتباط بین این دو هم توسط خواص SectionName و یا SectionIdهای متناظر، برقرار میشود. اگر چندین SectionContent دارای نام و یا Id یکسانی باشند، محتوای آخرین آنها در SectionOutlet متناظر، رندر میشود.
برای مثال در فایل MainLayout.razor، تغییر زیر را اعمال میکنیم:
که در آن یک SectionOutlet، با نام before-top-row اضافه شدهاست و سبب درج محتوایی پیش از لینک About میشود. پس از این تعریف، اکنون در هر کامپوننتی از برنامه میتوان محتوای این قسمت را به نحو زیر تامین کرد:
همانطور که مشخص است، این محتوا بر اساس نام ذکر شدهی متناظر با نام SectionOutlet، با آن ارتباط برقرار میکند.
روش تعریف یک محتوای پیشفرض
این محتوا، فقط زمانی تامین خواهد شد که کامپوننت در حال نمایش SectionContent، قابل مشاهده و فعال شده باشد. یعنی اگر از کامپوننت نمایش دهندهی آن به صفحهی دیگری رجوع کنیم، محتوای SectionOutlet مجددا خالی خواهد شد، تا زمانیکه به آدرس نمایش دهندهی کامپوننت دربرگیرندهی SectionContent متناظر با آن رجوع کنیم. به همین جهت اگر علاقمند به نمایش یک «محتوای پیشفرض» هستید، میتوان به صورت زیر عمل کرد:
به این ترتیب حتی اگر در لحظهی نمایش کامپوننت جاری، هیچ SectionContent متناظری یافت نشد، از همان SectionContent ذیل این SectionOutlet، برای تامین محتوای آن استفاده میشود و اگر کامپوننتی محتوای جدیدی را تعریف کرد، چون همیشه آخرین SectionContent رندر شده، محتوای نهایی را تامین میکند، این محتوای جدید، جایگزین نمونهی پیشفرض خواهد شد.
تفاوت SectionId با SectionName
کامپوننت SectionOutlet، هم میتواند یک SectionName را دریافت کند و هم یک SectionId را. SectionNameها، رشتهای هستند و تغییرات آتی آنها تحت نظارت کامپایلر نیست. به همین جهت اگر نیاز به Refactoring آنها است، شاید بهتر باشد از خاصیت SectionId که یک Id از نوع strongly typed را مشخص میکند، استفاده کنیم.
برای نمونه میتوان مثال قبلی را به صورت زیر با استفاده از یک SectionId، بازنویسی کرد:
که در اینجا SectionId، یک فیلد استاتیک از نوع SectionOutlet است.
سپس هر کامپوننت دیگری که بخواهد از این Id استاتیک استفاده کند، روش کار آن به صورت زیر است:
کامپوننتهای جدید SectionOutlet و SectionContent در Blazor 8x
پیاده سازی Sections در Blazor 8x به کمک دو کامپوننت جدید SectionOutlet و SectionContent میسر شدهاست و برای دسترسی به آنها نیاز است ابتدا به فایل Imports.razor_ پروژه، مراجعه کرد و using زیر را به آن اضافه نمود تا این اشیاء، در کامپوننتهای برنامه قابل شناسایی و استفاده شوند:
@using Microsoft.AspNetCore.Components.Sections
SectionOutlet کامپوننتی است که محتوای ارائه شدهی توسط کامپوننت SectionContent را رندر میکند (این محتوا در اصل یک RenderFragment است). ارتباط بین این دو هم توسط خواص SectionName و یا SectionIdهای متناظر، برقرار میشود. اگر چندین SectionContent دارای نام و یا Id یکسانی باشند، محتوای آخرین آنها در SectionOutlet متناظر، رندر میشود.
برای مثال در فایل MainLayout.razor، تغییر زیر را اعمال میکنیم:
<div class="top-row px-4"> <SectionOutlet SectionName="before-top-row"/> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div>
<SectionContent SectionName="before-top-row"> <a href="/show-help" target="_blank">Help</a> </SectionContent>
روش تعریف یک محتوای پیشفرض
این محتوا، فقط زمانی تامین خواهد شد که کامپوننت در حال نمایش SectionContent، قابل مشاهده و فعال شده باشد. یعنی اگر از کامپوننت نمایش دهندهی آن به صفحهی دیگری رجوع کنیم، محتوای SectionOutlet مجددا خالی خواهد شد، تا زمانیکه به آدرس نمایش دهندهی کامپوننت دربرگیرندهی SectionContent متناظر با آن رجوع کنیم. به همین جهت اگر علاقمند به نمایش یک «محتوای پیشفرض» هستید، میتوان به صورت زیر عمل کرد:
<div class="top-row px-4"> <SectionOutlet SectionName="before-top-row" /> <SectionContent SectionName="before-top-row"> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </SectionContent> </div>
تفاوت SectionId با SectionName
کامپوننت SectionOutlet، هم میتواند یک SectionName را دریافت کند و هم یک SectionId را. SectionNameها، رشتهای هستند و تغییرات آتی آنها تحت نظارت کامپایلر نیست. به همین جهت اگر نیاز به Refactoring آنها است، شاید بهتر باشد از خاصیت SectionId که یک Id از نوع strongly typed را مشخص میکند، استفاده کنیم.
برای نمونه میتوان مثال قبلی را به صورت زیر با استفاده از یک SectionId، بازنویسی کرد:
<div class="top-row px-4"> <SectionOutlet SectionId="BeforeTopRow" /> <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a> </div> @code{ public static SectionOutlet BeforeTopRow = new(); }
سپس هر کامپوننت دیگری که بخواهد از این Id استاتیک استفاده کند، روش کار آن به صورت زیر است:
<SectionContent SectionId="MainLayout.BeforeTopRow"> <a href="/show-help" target="_blank">Help</a> </SectionContent>
مطالب
بررسی تغییرات Blazor 8x - قسمت هشتم - مدیریت انتقال اطلاعات Pre-Rendering سمت سرور، به جزایر تعاملی
این Prerendering است که امکان رندر یک کامپوننت تعاملی را در سمت سرور میسر میکند تا کاربر بتواند پیش از فعال شدن قابلیتهای پیشرفتهی یک کامپوننت، یک حداقل خروجی را از آن مشاهده کند و همچنین وجود آن برای موتورهای جستجو و بهبود SEO بسیار مفید است. اما ... در این بین مشکلی رخ میدهد که نمونهی آنرا در قسمت قبل مشاهده کردیم: آغاز آن دوبار صورت میگیرد؛ یکبار در سمت سرور برای تهیهی یک خروجی SSR و یکبار هم پس از فعال شدن قابلیتهای تعاملی آن در سمت کلاینت. این آغاز دوباره، برای هر دو حالت کامپوننتهای تعاملی Blazor Server و Blazor WASM برقرار است. راهحلهایی از نحوهی مواجه شدن با یک چنین مشکلی را در قسمت قبل بررسی کردیم. راهحل دیگری که در این بین ارائه شده و توسط خود مایکروسافت هم در مثالهای آن مورد استفاده قرار میگیرد، استفاده از سرویس PersistentComponentState است که جزئیات آنرا در این قسمت بررسی خواهیم کرد.
بررسی نحوهی عملکرد سرویس PersistentComponentState
سرویس PersistentComponentState، در داتنت 6، به Blazor اضافه شد و امکان جدیدی نیست. قسمتی از این مباحث جدید SSR که بهنظر مختص به Blazor 8x هستند، پیشتر هم وجود داشتند؛ تحت عنوان pre-rendering. برای مثال فقط کافی بودن تا در برنامههای Blazor Server قبلی، فایل Host.cshtml_ را به صورت زیر ویرایش کرد تا pre-rendering فعال شود:
مشکلی که در این حالت بروز میکرد این بود که متد OnInitializedAsync یک کامپوننت، دوبار فراخوانی میشد؛ یکبار در زمان pre-rendering در سمت سرور، تا HTML استاتیکی برای ارائهی به مرورگر کاربر تولید شود و بار دیگر در زمان فعال شدن اتصال SignalR کامپوننت و ارائهی نهایی تعاملی آن. به همین جهت، کار سنگین آغازین یک کامپوننت، دوبار انجام میشد که تجربهی کاربری ناخوشایندی را هم به همراه داشت. برای رفع این مشکل، tag helper ویژهای را به نام persist-component-state تهیه کردند که به صورت زیر به فایل host.cshtml_ اضافه میشد:
این tag-helper فوق است که کار درج رمزنگاری شدهی اطلاعات کش شدهی pre-rendering سمت سرور را در انتهای فایل HTML صفحه انجام میدهد و برای نمونه، نحوهی استفادهی از آن به صورت زیر است:
توضیحات:
- ابتدا تزریق سرویس 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 صفحه مواجه شوید!
فرمت این اطلاعات، JsonSerializer.SerializeToUtf8Bytes رمزنگاری شدهی توسط IDataProtectionProvider است. این هم یک روش نگهداری اطلاعات، بجای استفاده از کش مرورگر است؛ بدون نیاز به استفاده از جاوااسکریپت و کار با local storage و امثال آن.
مایکروسافت از این نوع کارها پیشتر در ASP.NET Web forms توسط ViewStateها انجام دادهاست! البته ViewStateها جهت نگهداری اطلاعات وضعیت کلاینت، به سمت سرور ارسال میشوند؛ اما این Component-Stateهای مخفی، فقط یکبار توسط قسمت کلاینت خوانده میشوند و هدف آنها ارسال اطلاعاتی به سمت سرور نیست.
یک نکته: همانطور که عنوان شد، در نگارشهای قبلی Blazor، از تگهلپری به نام persist-component-state برای درج این اطلاعات در انتهای صفحه استفاده میکردند. استفاده از این تگهلپر در Blazor 8x منسوخ شده و به صورت خودکار دادههای تمام سرویسهای PersistentComponentState فعالی که توسط PersistAsJson اطلاعاتی را ذخیره میکنند، جمع آوری و به صورت یکجا در انتهای صفحه به صورت رمزنگاری شده، درج میکنند.
روش خاموش کردن Pre-rendering
برای خاموش کردن pre-rendering نیاز است پارامتر همنامی را به نحو زیر با false مقدار دهی کرد:
بازنویسی مثال قسمت قبل با استفاده از سرویس PersistentComponentState
در قسمت قبل هرچند روش مواجه شدن با مشکل دوبار فراخوانی شدن متد آغازین یک کامپوننت را در سمت سرور و کلاینت بررسی کردیم، اما ... در نهایت دوبار مراجعه به بانک اطلاعاتی انجام میشود؛ یکبار در زمان pre-rendering و با مراجعهی مستقیم به یک سرویس سمت سرور و یکبار دیگر هم در زمان فراخوانی httpClient.GetFromJsonAsync در سمت کلاینت برای دریافت اطلاعات مورد نیاز از یک Web API Endpoint. اگر بخواهیم اطلاعات لیست محصولات دریافتی سمت سرور را به سمت کلاینت منتقل کنیم و مجددا آنرا از بانک اطلاعاتی دریافت نکنیم، راهحل سوم آن، استفاده از سرویس PersistentComponentState است.
در کدهای زیر، بازنویسی کامپوننت محصولات مشابه را مشاهده میکنید که کمی پیچیدهتر شدهاست. اینبار لیست محصولات مشابه، در همان بار اول رندر کامپوننت نمایش داده میشوند و نه پس از کلیک بر روی دکمهای. به همین جهت باید کار مدیریت دوبار فراخوانی متد رویدادگردان OnInitializedAsync را به درستی انجام داد. متد OnInitializedAsync یکبار در حین پیشرندر سمت سرور اجرا میشود و بار دیگر زمانیکه وباسمبلی جاری در مرورگر به صورت کامل دریافت شده و فعال میشود؛ یعنی برای بار دوم، کدهای اجرایی آن سمت کلاینت هستند.
در این مثال با استفاده از سرویس PersistentComponentState، اطلاعات دریافت شدهی در حین pre-rendering ابتدایی رخدادهی در سمت سرور، در متد OnPersistingAsync، کش شده و در حین فعال شدن وباسمبلی مرتبط در سمت کلاینت، با استفاده از متد PersistentState.TryTakeFromJson، از کش خوانده میشوند و دیگر دوبار رفت و برگشت به بانک اطلاعاتی را شاهد نخواهیم بود.
در این پیاده سازی هنوز هم از سرویس IProductStore استفاده میشود که دو نگارش سمت سرور و سمت کلاینت آنرا در قسمت قبل تهیه کردیم. برای مثال باتوجه به اینکه کدهای فوق در حین pre-rendering در سمت سرور اجرا میشوند، به صورت خودکار از نگارش سمت سرور IProductStore استفاده خواهد شد.
نکتهی مهم: فعلا کدهای فوق فقط برای حالت بارگذاری اولیهی کامپوننت درست کار میکنند. یعنی اگر نگارش وباسمبلی کامپوننت پس از وقوع پیشرندر سمت سرور، در مرورگر دریافت و بارگذاری کامل شود؛ اما برای دفعات بعدی مراجعهی به این صفحه با استفاده از enhanced navigation و راهبری بهبود یافته که در قسمت ششم این سری بررسی کردیم ... دیگر کار نمیکنند و در این حالت کش شدنی رخ نمیدهد که در نتیجه، شاهد دوبار رفت و برگشت به بانک اطلاعاتی خواهیم بود و اساسا استفادهی از PersistentComponentState را زیر سؤال میبرد؛ چون در حین راهبری بهبود یافته، از آن استفاده نمیشود (قسمت PersistentState.TryTakeFromJson آن، هیچگاه اطلاعاتی را از کش نمیخواند). اطلاعات بیشتر
بررسی نحوهی عملکرد سرویس 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 آن، هیچگاه اطلاعاتی را از کش نمیخواند). اطلاعات بیشتر
مسیرراهها
ویژگیهای C# 8.0
فایلهای پروژهها
PdfRpt-2.7.rar
- Added .NET 3.5, 4.0 and 4.5 profiles to the NuGet package. PM> Install-Package PdfReport - Updated the project to use itextsharp.dll 5.5.4.0. - Fixed issue #2392. - Fixed reopening of MemoryStreams. - Added `Merge In-Memory Pdf Files` sample. - Improved Grouping/GroupingPdfReport.cs sample to show how to inject an arbitrary HTML data at the end of the each group.