انتشار TypeScript 3.3
کنترولر های چاق در ASP.NET MVC!
Want to learn about the latest and greatest in the 64-bit Visual Studio 2022? Join Scott Hanselman and Visual Studio product team as they take Visual Studio 2022 for a spin.
20:27 Profiling .NET apps in Visual Studio 2022
23:19 Cross platform apps with WSL and CMake in Visual Studio 2022
26:07 Testing your .NET app on Linux
28:00 Easily create CI/CD pipelines using GitHub actions with Visual Studio 2022
30:40 Balloon drop!
This article explains how to get started with WebSockets in ASP.NET Core. WebSocket is a protocol that enables two-way persistent communication channels over TCP connections. It is used for applications such as chat, stock tickers, games, anywhere you want real-time functionality in a web application.
کتابخانه alton
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{ "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70", "iss": "https://localhost:5001/", "iat": 1576402824, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.", "DisplayName": "Vahid N.", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1576402824, "exp": 1576402944, "aud": "Any" }
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
class App extends Component { state = {};
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
> npm install --save jwt-decode
import jwtDecode from "jwt-decode"; // ... class App extends Component { state = {}; componentDidMount() { try { const jwt = localStorage.getItem("token"); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); this.setState({ currentUser }); } catch (ex) { console.log(ex); } }
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
render() { return ( <React.Fragment> <ToastContainer /> <NavBar user={this.state.currentUser} /> <main className="container">
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
const NavBar = ({ user }) => {
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/login"> Login </NavLink> <NavLink className="nav-item nav-link" to="/register"> Register </NavLink> </React.Fragment> )}
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink> <NavLink className="nav-item nav-link" to="/profile"> {user.DisplayName} </NavLink> </React.Fragment> )}
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
window.location = "/";
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink>
import Logout from "./components/logout"; // ... class App extends Component { render() { return ( // ... <Switch> // ... <Route path="/logout" component={Logout} />
import { Component } from "react"; class Logout extends Component { componentDidMount() { localStorage.removeItem("token"); window.location = "/"; } render() { return null; } } export default Logout;
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token"; export async function login(email, password) { const { data: { access_token } } = await http.post(apiEndpoint + "/login", { email, password }); console.log("JWT", access_token); localStorage.setItem(tokenKey, access_token); }
const { data } = this.state; await auth.login(data.username, data.password); window.location = "/";
همینکار را برای logout نیز در authService انجام داده:
export function logout() { localStorage.removeItem(tokenKey); }
import * as auth from "../services/authService"; class Logout extends Component { componentDidMount() { auth.logout();
import jwtDecode from "jwt-decode"; //... export function getCurrentUser() { try { const jwt = localStorage.getItem(tokenKey); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); return currentUser; } catch (ex) { console.log(ex); return null; } }
import * as auth from "./services/authService"; class App extends Component { state = {}; componentDidMount() { const currentUser = auth.getCurrentUser(); this.setState({ currentUser }); }
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
export function loginWithJwt(jwt) { localStorage.setItem(tokenKey, jwt); }
import * as auth from "../services/authService"; // ... const response = await userService.register(this.state.data); auth.loginWithJwt(response.headers["x-auth-token"]);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
مدل برنامه، جهت تامین دادههای خود ارجاع دهنده و درختی
فرض کنید قصد داریم لیستی از کامنتهای تو در تو را مدل سازی کنیم که در آن هر کامنت، میتواند چندین کامنت تا بینهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels; public class Comment { public IList<Comment> Comments = new List<Comment>(); public string? Text { set; get; } }
using BlazorTreeView.ViewModels; namespace BlazorTreeView.Pages; public partial class TreeView { private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } = new Dictionary<string, object>(StringComparer.Ordinal) { { "style", "list-style: none;" }, }; private IList<Comment> Comments { get; } = new List<Comment> { new() { Text = "پاسخ یک", }, new() { Text = "پاسخ دو", Comments = new List<Comment> { new() { Text = "پاسخ اول به پاسخ دو", Comments = new List<Comment> { new() { Text = "پاسخی به پاسخ اول پاسخ دو", }, }, }, new() { Text = "پاسخ دوم به پاسخ دو", }, }, }, new() { Text = "پاسخ سوم", }, }; }
طراحی کامپوننت DntTreeView
برای اینکه بتوانیم به یک کامپوننت با قابلیت استفادهی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary> /// A custom DntTreeView /// </summary> public partial class DntTreeView<TRecord> {
/// <summary> /// The treeview's self-referencing items /// </summary> [Parameter] public IEnumerable<TRecord>? Items { set; get; }
/// <summary> /// The treeview item's template /// </summary> [Parameter] public RenderFragment<TRecord>? ItemTemplate { set; get; }
/// <summary> /// The content displayed if the list is empty /// </summary> [Parameter] public RenderFragment? EmptyContentTemplate { set; get; }
public class Comment { public IList<Comment> Comments = new List<Comment>(); public string? Text { set; get; } }
/// <summary> /// The property which returns the children items /// </summary> [Parameter] public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
<DntTreeView TRecord="Comment" Items="Comments" ChildrenSelector="m => m.Comments"
public partial class DntTreeView<TRecord> { private Expression? _lastCompiledExpression; internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; } // ... protected override void OnParametersSet() { if (_lastCompiledExpression != ChildrenSelector) { CompiledChildrenSelector = ChildrenSelector?.Compile(); _lastCompiledExpression = ChildrenSelector; } } }
@namespace BlazorTreeView.Pages.Components @typeparam TRecord @if (Items is null || !Items.Any()) { @EmptyContentTemplate } else { <CascadingValue Value="this"> <ul @attributes="AdditionalAttributes"> @foreach (var item in Items) { <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/> } </ul> </CascadingValue> }
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام میشود که آنهم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت میکند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده میکنید. این روش، یکی از روشهای اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننتهای فرزند است که در ادامه از آن استفاده خواهیم کرد.
تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor
اگر به حلقهی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمیدهد؛ اما کامنتهای ما چندسطحی و تو در تو هستند و عمق آنها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننتهای Blazor داشت که خوشبختانه این مورد پیشبینی شدهاست و هر کامپوننت Blazor، میتواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components @typeparam TRecord <li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()"> @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null) { @SafeOwnerTreeView.ItemTemplate(ParentItem) } @if (Children is not null) { <ul> @foreach (var item in Children) { <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/> } </ul> } </li>
کدهای پشت صحنهی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary> /// A custom DntTreeView /// </summary> public partial class DntTreeViewChildrenItem<TRecord> { /// <summary> /// Defines the owner of this component. /// </summary> [CascadingParameter] public DntTreeView<TRecord>? OwnerTreeView { get; set; } private DntTreeView<TRecord> SafeOwnerTreeView => OwnerTreeView ?? throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`."); /// <summary> /// Nested parent item to display /// </summary> [Parameter] public TRecord? ParentItem { set; get; } private IEnumerable<TRecord>? Children => ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null ? null : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem); }
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر میکند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آنرا با فراخوانی مجدد خودش ، رندر میکند.
روش استفاده از کامپوننت DntTreeView
اکنون که کار توسعهی کامپوننت جنریک DntTreeView پایان یافت، روش استفادهی از آن به صورت زیر است:
<div class="card" dir="rtl"> <div class="card-header"> DntTreeView </div> <div class="card-body"> <DntTreeView TRecord="Comment" Items="Comments" ChildrenSelector="m => m.Comments" style="list-style: none;" ChildrenHtmlAttributes="ChildrenHtmlAttributes"> <ItemTemplate Context="record"> <div class="card mb-1"> <div class="card-body"> <span>@record.Text</span> </div> </div> </ItemTemplate> <EmptyContentTemplate> <div class="alert alert-warning"> There is no item to display! </div> </EmptyContentTemplate> </DntTreeView> </div> </div>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافتهی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار میکند.