C# enthusiast. NET foundation member
ساخت چت روم با Blazor Web Assembly و SignalR قسمت دوم: ساخت کلاینت
در این مقاله قصد داریم به ادامه ساخت چت روم با ASP Net Core Web API و Blazor Web Assembly بپردازیم.
ابتدا به سراغ ساخت صفحه لاگین و رجیستر میرویم. در پوشه Pages یک razor component تحت عنوان Login میسازیم. سپس برای داشتن css isolation یک فایل دیگر با نام Login.razor.css ایجاد میکنیم. برای اینکه css isolation به درستی کار کند، قطعه کد زیر را در فایل index.html به قسمت head اضافه میکنیم.
قسمت ChatApplication.client در واقع نام assembly پروژه میباشد. سپس فایل های استاتیک شامل Font Awsome و Bootstrap را به index.html اضافه میکنیم. در حال حاضر index.html شامل کد های زیر می باشد.
پروژه Client را اجرا میکنیم. به صفحه لاگین میرویم و مشاهده میکنیم که استایل ها به درستی لود شده اند
استفاده از Local Storage در Blazor
برای استفاده از Access Token و ارتباط با Server نیاز داریم که توکن دریافتی را در جایی ذخیره کنیم. برای اینکار بهترین کار استفاده از قابلیت Local Storage در Browser می باشد. میتوانیم از قابلیت Javascript Interop در Blazor برای کار با Local Storage استفاده کنیم. اما پکیجی با نام Blazored.LocalStorage وجود دارد که کار با Local Storage در Blazor را بسیار راحت میکند. این پکیج را در پروژه Client نصب میکنیم.
همچنین در Package Manager Console نیز میتوانیم با دستور زیر این پکیج را نصب کنیم
Install-Package Blazored.LocalStorage -Version 4.1.5
سپس قطعه کد زیر را برای استفاده Local Storage به کلاس Program اضافه میکنیم.
جداسازی Code از UI
برای اینکه کد تمیزتری داشته باشیم میتوانیم کد مربوط به صفحه UI را از منطق پروژه جدا کنیم. می دانیم که هرکدام از razor فایل ها پس از کامپایل به یک partial class تبدیل میشوند. با ایجاد فایل با پسوند razor.cs و هم نام با فایل razor اصلی میتوانیم به نوعی یک code behind برای هر صفحه razor داشته باشیم. پس فایل Login.razor.cs را به شکل زیر ایجاد میکنیم.
حال میتوانیم منطق پروژه Client و قسمت های مربوط به بخش Login را در این کلاس بنویسیم.
ایجاد سرویس مربوط به Login
ابتدا در پروژه Client یک پوشه با نام Service ایجاد میکنیم. در پوشه یک پوشه دیگر با نام Login ایجاد میکنیم.
اینترفیس ILoginService را به شکل زیر ایجاد میکنیم.
حال کلاس LoginService را که این اینترفیس را پیاده سازی میکند به شکل زیر مینویسیم
ابتدا یک instance از HttpClient را تزریق میکنیم. از Extension Method ای به نام PostAsJsonAsync که مدل را به صورت JSON در Body به URL داده شده پست میکند استفاده میکنیم. اگر Response داده شده دارای Status Code 200 نبود null بر میگردانیم و در غیر این صورت Content آن را به صورت stream خوانده و به Access Token مدل را Deserialize میکنیم.
برای آنکه HttpClient در سرویس ما تزریق شود، تکه کد زیر را در کلاس Program پروژه Client اضافه میکنیم. قبل از آن نیاز داریم که پکیج Microsoft.Extensions.Http را به پروژه Client آضافه کنیم.
همچنین میتوانیم از دستور زیر در Package Manager Console استفاده کنیم.
Install-Package Microsoft.Extensions.Http -Version 5.0.0
استفاده از Toaster برای اعلام Notification ها به کاربر
مانند Local Storage ، برای استفاده از Toaster در Blazor نیز پکیج وجود دارد که نصب و تنظیم آن مانند پکیج قبلی کار بسیار سرراست و ساده ای است.
ابتدا پکیج Sotsera.Blazor.Toaster را دانلود و نصب میکنیم.
با استفاده از دستور زیر در Package Manager Console نیز میتوانیم این پکیج را نصب کنیم.
Install-Package Sotsera.Blazor.Toaster -Version 3.0.0
سپس در کلاس Program قطعه کد زیر را برای تنظیم Toaster اضافه میکنیم.
در اینجا Postion مربوط به Toaster را در قسمت بالا وسط تعیین کرده ایم. سپس به فایل index.html قطعه کد زیر را اضافه میکنیم.
سپس به فایل App.razor در قسمت بالا قطعه کد زیر را اضافه میکنیم
تکمیل کد های مربوط به بخش Login
حال که همه سرویس های مربوط به بخش Login آماده است به سراغ تکمیل آن می رویم. یادتان باشد که ما بخش Code را از UI جدا کرده ایم پس تمام کد های مربوط به بخش Login را در کلاس Login.razor.cs مینویسیم.
ابتدا سرویس های مورد نیاز را به این بخش تزریق میکنیم
در اینجا سرویس های مربوط به Local Storage و Toaster و Login Service را تزریق کرده ایم. صفت Inject در واقع کار تزریق را برای ما در Blazor انجام میدهد. برای Navigate کردن به سایر صفحات نیز Navigation Manager را Inject میکنیم
سپس یک پراپرتی از جنس LoginViewModel را new کرده و در کلاس قرار میدهیم. برای اینکه در تگ EditForm در Blazor این مدل قابل استفاده باشد نیاز است که آن را new کنیم.
حال متد OnInitializedAsync را override میکنیم. این متد زمانی که صفحه رندر و Initialize می شود اجرا میشود.
در اینجا در Local Storage چک میکنیم که آیا کلیدی با نام userToken وجود دارد یا خیر. اگر وجود داشت به معنی این است که یوزر از قبل لاگین کرده است و آن را به صفحه اصلی میفرستیم.
در قسمت UI برای اینکه فرم Login به درستی کار کند تغییرات زیر را انجام میدهیم
در خط 4 با استفاده از Component ای به نام DataAnnotationsValidator قابلیت Data Annotation Validation را به فرم اضافه میکنیم.
در خط 8 و 12 با استفاده از InputText Componenet مقادیر را به پراپرتی های LoginViewModel بایند میکنیم و همچنین با استفاده از ValidationMessage پیامی که هنگام خالی بودن InputText باید نمایش داده شود را نمایش میدهیم. متد HandleValidSubmit را به شکل زیر پیاده سازی میکنیم.
اگر Token برابر null بود با Toaster به کاربر نشان میدهیم که کاربر یافت نشده است و در غیر این صورت مقدار Local Storage را برابر Access Token قرار میدهیم.
قبل از هرچیزی کلاس AccessToken را به شکل زیر باید تغییر دهیم
باید یک Constructor هنگام deserialize کردن مقدار Json در نظر بگیریم.
حال فرم Login را چک میکنیم. پروژه را اجرا میکنیم. ابتدا مقادیر username و password را خالی می گذاریم و مشاهده میکنیم که validation ها درست کار میکنند.
حال مقدار username و password را با مقادیر اشتباه پر میکنیم و مشاهده میکنیم که Toaster هم درست کار میکند.
حال با مقادیر درست username و password را پر میکنیم. مشاهده میشود که در local storage مرورگر مقدار Token ذخیره شده است.
به user_id برای جداسازی چت های کاربر و سایر کاربران نیاز داریم. در اینجا برای راحتی کار user_id را داخل توکن قرار داریم ولی این کار باعث بوجود آمدن مشکلات امنیتی میشود. پیشنهاد میکنم که از GUID برای اینکار استفاده کنید و یا راه حل دیگری برای تشخیص یوزر جاری در نظر بگیرید.
به همین منوال میتوانیم صفحه مربوط به رجیستر و ثبت نام کاربر را نیز بسازیم.
ساخت صفحه چت کاربران
در فولدر Pages یک razor Component به نام Index می سازیم. کد اولیه این صفحه به صورت زیر خواهد بود.
نوشتن کد های مربوط به Chat و SignalR Client
ابتدا پکیج مربوط به SignalR Client را نصب میکنیم.
از طریق Package Manager Console نیز با دستور زیر میتوانیم SignalR را نصب کنیم.
Install-Package Microsoft.AspNetCore.SignalR.Client -Version 5.0.9
سپس سرویس مربوط به Chat را مینویسیم. در فولدر Service یک فولدر دیگر با نام Chat ایجاد میکنیم و در آن اینترفیس IChatService را به شکل زیر مینویسیم.
سپس کلاس ChatService را به که این اینترفیس را پیاده سازی میکند به شکل زیر مینویسیم.
در خط 14 برای احراز هویت، توکن را به صورت bearer در Header قرار داده ایم.
سپس در کلاس Program.cs این سرویس را به شکل زیر رجیستر میکنیم.
سپس کلاس Index.razor.cs را به شکل زیر میسازیم
ابتدا باید از IAsyncDisposable ارث بری کنیم چرا که نیاز داریم در پایان طول عمر Component مقادیر Hub و Debounce Timer را Dispose کنیم.
سپس در خط 5 یک فیلد از Hub Connection تعریف میکنیم.
سپس برای اینکه یک تاخیر هنگام تایپ کاربر و Notify کردن سایر کاربران ایجاد کنیم یک Debounce Timer تعریف میکنیم که پس از هر 500 میلی ثانیه یک متد را صدا میزند.
در خط 13 یک فیلد از جنس string تعریف میکنیم که مقدار پیام وروردی کاربر به آن bind می شود.
در خط 14 یک فیلد از جنس int داریم که ID کاربر را در آن ذخیره میکنیم. از این فیلد بعد ها برای تمایز بین پیام های کاربر و سایر کاربران استفاده خواهیم کرد.
در خط 15 یک لیست از جنس UserMessageViewModel داریم که تاریخچه چت را به کاربر نشان میدهد.
از خط 19 تا 22 سرویس های مورد نیاز را تزریق میکنیم.
کار اصلی از override کردن متد OnInitializedAsync شروع میشود. ابتدا بررسی میکنیم که آیا کاربر در Browser و Local Storage مقادیر توکن را دارد یا خیر و اگر نداشت، وی را به صفحه Login هدایت میکنیم. سپس فیلد _userId را با استفاده از متد GetUserIdAsync که در خط 82 تعریف شده است از Local Storage خوانده و مقدار دهی میکنیم. سپس مقدار _chatHistory را بوسیله سرویسی که قبلا نوشتیم از Web API خوانده و مقدار دهی میکنیم. سپس event مربوطه به هنگامی که debounce timer مقدار 500 میلی ثانیه را سپری میکند را صدا میزند مقدار دهی میکنیم. متد IsTyping فانکشن مربوطه را در SignalR Server فراخوانی میکند. سپس نوبت به مقداردهی _hub میرسد. در خط 39 مقادیر مربوط به URL را مقداردهی میکنیم. در خط 40 مقدار Access Token را بوسیله متد GetAccessTokenValueAsync از Local Storage خوانده و مقدار دهی میکنیم. سپس متد هایی که Hub روی آنها قرار است invoke بشود را تعریف میکنیم. ابتدا بوسیله Toaster هنگامی که کاربری به SignalR Server متصل میشود را نشان میدهیم. سپس متدی که هنگام دریافت پیام جدید صدا زده می شود را تعریف میکنیم. هنگام دریافت پیام جدید ، آن را به Chat History اضافه میکنیم و برای rerender کردن UI متد StateHasChanged را صدا میزنیم. سپس برای Notify کردن سایر کاربران هنگامی که یک یوزر تایپ میکند ، متد UpdateUserIsTypingAsync را می سازیم .متد InvokeAsync در خط 56 یک اکشن ورودی برای رندر کردن UI دریافت میکند. در آن مقدار typing user را از Server Hub دریافت کرده و برای نمایش آن سمت UI متد StateHasChanged را صدا میزنیم. پس از گذشت 1 ثانیه مقدار typing user را خالی کرده و مجددا StateHasChanged را صدا میزنیم . در نهایت در خط 48 Hub را استارت میکنیم.
در خط 89 متدی که هنگام زدن دکمه ارسال باید فراخوانی بشود را تعریف میکنیم. اگر مقدار message خالی نبود آن را به متد OnNewMessage در SignalR Server میفرستیم. سپس مقدار message را خالی میکنیم و متد StateHasChanged را صدا میزنیم.
در خط 101 یک متد برای on input event تعریف میکنیم که وقتی کاربر در حال تایپ در Text Area است صدا زده میشود. در آن یک بار Debounce Timer را متوقف میکنیم و مجددا استارت میزنیم.
در نهایت در خط 114 مقدار Hub و Timer را Dispose میکنیم.
نوشتن کد های مربوط به UI صفحه چت
فایل Index.razor را به شکل زیر بازنویسی میکنیم.
ابتدا در خط 6 چک میکنیم که اگر مقدار typing user خالی نبود ، آن را در قسمت بالای صفحه نمایش دهد.
سپس بین چت های موجود در Chat History پیمایش میکنیم و مسیج های کاربر و سایر کاربرها را نمایش میدهیم.
در خط 46 مقدار text area را به message بایند میکنیم و event مربوط به را به متد InitiateUserIsTyping که قبلا تعریف کرده بودیم Assign میکنیم.
در خط 47 نیز event مربوط به که برای Button تعریف شده است را به متد SendMessage که قبلا تعریف کرده ایم Assign میکنیم.
تست نهایی
ابتدا دو مرورگر باز میکنیم و در هر دو آنها Login میکنیم. سپس مشاهده میکنیم که نشان دادن کاربر آنلاین و همچنین ارسال پیام و نشان دادن کاربر در حال تایپ به درستی کار میکند.
جمع بندی
در این پروژه یک سرور از صفر با استفاده از Identity و Signal R و همچنین JWT Authentication ساختیم. همچنین به طور اجمالی با Blazor web assembly و نحوه استفاده از JWT Token و ارتباط با Signal R آشنا شدیم. شدیدا توصیه میکنم که کد های مربوط به این مقاله را از گیت هاب دریافت کرده و آن را روی سیستم خود اجرا و دیباگ کنید. در نهایت اگر سوال یا نظری داشتید، خوشحال میشوم که آن را در بخش نظرات این مقاله مطرح کنید.
مقالات بیشتر در دات نت زوم
مطلبی دیگر از این انتشارات
C# 9.0: Records - کار با دادههای تغییر ناپذیر کلاسها
مطلبی دیگر از این انتشارات
Directory browsing چیست؟
مطلبی دیگر از این انتشارات
مهم ترین اخبار رویداد NET Conf. با موضوع Focus on Blazor