Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایهی اعتبارسنجی و احراز هویت
- «معرفی JSON Web Token»
توسعهی IdentityUser
در قسمتهای 21 تا 23، روش نصب و یکپارچگی ASP.NET Core Identity را با یک برنامهی Blazor Server بررسی کردیم. در پروژهی Web API جاری هم از قصد داریم از ASP.NET Core Identity استفاده کنیم؛ البته بدون نصب UI پیشفرض آن. به همین جهت فقط از ApplicationDbContext آن برنامه که از IdentityDbContext مشتق شده و همچنین قسمتی از تنظیمات سرویسهای ابتدایی آن که در قسمت قبل بررسی کردیم، در اینجا استفاده خواهیم کرد.
IdentityUser پیشفرض که معرف موجودیت کاربران یک سیستم مبتنی بر ASP.NET Core Identity است، برای ثبت نام یک کاربر، فقط به ایمیل و کلمهی عبور او نیاز دارد که نمونهای از آنرا در حین معرفی «ثبت کاربر ادمین Identity» بررسی کردیم. اکنون میخواهیم این موجودیت پیشفرض را توسعه داده و برای مثال نام کاربر را نیز به آن اضافه کنیم. برای اینکار فایل جدید BlazorServer\BlazorServer.Entities\ApplicationUser .cs را به پروژهی Entities با محتوای زیر اضافه میکنیم:
using Microsoft.AspNetCore.Identity; namespace BlazorServer.Entities { public class ApplicationUser : IdentityUser { public string Name { get; set; } } }
اکنون که یک ApplicationUser سفارشی را ایجاد کردیم، نیازی نیست تا DbSet خاص آنرا به ApplicationDbContext برنامه اضافه کنیم. برای معرفی آن به برنامه ابتدا باید به فایل BlazorServer\BlazorServer.DataAccess\ApplicationDbContext.cs مراجعه کرده و نوع IdentityUser را به IdentityDbContext، از طریق آرگومان جنریکی که میپذیرد، معرفی کنیم:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
پس از این تغییر، در فایل BlazorWasm\BlazorWasm.WebApi\Startup.cs نیز باید ApplicationUser را به عنوان نوع جدید کاربران، معرفی کرد:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // ...
پس از این تغییرات، باید از طریق خط فرمان به پوشهی BlazorServer.DataAccess وارد شد و دستورات زیر را جهت ایجاد و اعمال Migrations متناظر با تغییرات فوق، اجرا کرد. چون در این دستورات اینبار پروژهی آغازین، به پروژهی Web API اشاره میکند، باید بستهی نیوگت Microsoft.EntityFrameworkCore.Design را نیز به پروژهی آغازین اضافه کرد، تا بتوان آنها را با موفقیت به پایان رساند:
dotnet tool update --global dotnet-ef --version 5.0.4 dotnet build dotnet ef migrations --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ add AddNameToAppUser --context ApplicationDbContext dotnet ef --startup-project ../../BlazorWasm/BlazorWasm.WebApi/ database update --context ApplicationDbContext
ایجاد مدلهای ثبت نام
در ادامه میخواهیم کنترلری را ایجاد کنیم که کار ثبت نام و لاگین را مدیریت میکند. برای این منظور باید بتوان از کاربر، اطلاعاتی مانند نام کاربری و کلمهی عبور او را دریافت کرد و پس از پایان عملیات نیز نتیجهی آنرا بازگشت داد. به همین جهت دو مدل زیر را جهت مدیریت قسمت ثبت نام، به پروژهی BlazorServer.Models اضافه میکنیم:
using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class UserRequestDTO { [Required(ErrorMessage = "Name is required")] public string Name { get; set; } [Required(ErrorMessage = "Email is required")] [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid email address")] public string Email { get; set; } public string PhoneNo { get; set; } [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] public string Password { get; set; } [Required(ErrorMessage = "Confirm password is required")] [DataType(DataType.Password)] [Compare("Password", ErrorMessage = "Password and confirm password is not matched")] public string ConfirmPassword { get; set; } } }
public class RegistrationResponseDTO { public bool IsRegistrationSuccessful { get; set; } public IEnumerable<string> Errors { get; set; } }
ایجاد و تکمیل کنترلر Account، جهت ثبت نام کاربران
در ادامه نیاز داریم تا جهت ارائهی امکانات اعتبارسنجی و احراز هویت کاربران، کنترلر جدید Account را به پروژهی Web API اضافه کنیم:
using System; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using System.Threading.Tasks; using System.Linq; using BlazorServer.Common; namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] [Authorize] public class AccountController : ControllerBase { private readonly SignInManager<ApplicationUser> _signInManager; private readonly UserManager<ApplicationUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; public AccountController(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager) { _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager)); } [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignUp([FromBody] UserRequestDTO userRequestDTO) { var user = new ApplicationUser { UserName = userRequestDTO.Email, Email = userRequestDTO.Email, Name = userRequestDTO.Name, PhoneNumber = userRequestDTO.PhoneNo, EmailConfirmed = true }; var result = await _userManager.CreateAsync(user, userRequestDTO.Password); if (!result.Succeeded) { var errors = result.Errors.Select(e => e.Description); return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false }); } var roleResult = await _userManager.AddToRoleAsync(user, ConstantRoles.Customer); if (!roleResult.Succeeded) { var errors = result.Errors.Select(e => e.Description); return BadRequest(new RegistationResponseDTO { Errors = errors, IsRegistrationSuccessful = false }); } return StatusCode(201); // Created } } }
- در تعریف ابتدایی این کنترلر، ویژگیهای زیر ذکر شدهاند:
[Route("api/[controller]/[action]")] [ApiController] [Authorize]
تا اینجا اگر برنامه را اجرا کنیم، میتوان با استفاده از Swagger UI، آنرا آزمایش کرد:
که با اجرای آن، برای نمونه به خروجی زیر میرسیم:
که عنوان میکند کلمهی عبور باید حداقل دارای یک عدد و یک حرف بزرگ باشد. پس از اصلاح آن، status-code=201 را دریافت خواهیم کرد.
و اگر سعی کنیم همین کاربر را مجددا ثبت نام کنیم، با خطای زیر مواجه خواهیم شد:
ایجاد مدلهای ورود به سیستم
در پروژهی Web API، از UI پیشفرض ASP.NET Core Identity استفاده نمیکنیم. به همین جهت نیاز است مدلهای قسمت لاگین را به صورت زیر تعریف کنیم:
using System.ComponentModel.DataAnnotations; namespace BlazorServer.Models { public class AuthenticationDTO { [Required(ErrorMessage = "UserName is required")] [RegularExpression("^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid email address")] public string UserName { get; set; } [Required(ErrorMessage = "Password is required.")] [DataType(DataType.Password)] public string Password { get; set; } } }
using System.Collections.Generic; namespace BlazorServer.Models { public class AuthenticationResponseDTO { public bool IsAuthSuccessful { get; set; } public string ErrorMessage { get; set; } public string Token { get; set; } public UserDTO UserDTO { get; set; } } public class UserDTO { public string Id { get; set; } public string Name { get; set; } public string Email { get; set; } public string PhoneNo { get; set; } } }
ایجاد مدل مشخصات تولید JSON Web Token
پس از لاگین موفق، نیاز است یک JWT را تولید کرد و در اختیار کلاینت قرار داد. مشخصات ابتدایی تولید این توکن، توسط مدل زیر تعریف میشود:
namespace BlazorServer.Models { public class BearerTokensOptions { public string Key { set; get; } public string Issuer { set; get; } public string Audience { set; get; } public int AccessTokenExpirationMinutes { set; get; } } }
{ "BearerTokens": { "Key": "This is my shared key, not so secret, secret!", "Issuer": "https://localhost:5001/", "Audience": "Any", "AccessTokenExpirationMinutes": 20 } }
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddOptions<BearerTokensOptions>().Bind(Configuration.GetSection("BearerTokens")); // ...
ایجاد سرویسی برای تولید JSON Web Token
سرویس زیر به کمک سرویس توکار UserManager مخصوص Identity و مشخصات ابتدایی توکنی که معرفی کردیم، کار تولید یک JWT را انجام میدهد:
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Threading.Tasks; using BlazorServer.Entities; using BlazorServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; namespace BlazorServer.Services { public interface ITokenFactoryService { Task<string> CreateJwtTokensAsync(ApplicationUser user); } public class TokenFactoryService : ITokenFactoryService { private readonly UserManager<ApplicationUser> _userManager; private readonly BearerTokensOptions _configuration; public TokenFactoryService( UserManager<ApplicationUser> userManager, IOptionsSnapshot<BearerTokensOptions> bearerTokensOptions) { _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); if (bearerTokensOptions is null) { throw new ArgumentNullException(nameof(bearerTokensOptions)); } _configuration = bearerTokensOptions.Value; } public async Task<string> CreateJwtTokensAsync(ApplicationUser user) { var signingCredentials = new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration.Key)), SecurityAlgorithms.HmacSha256); var claims = await getClaimsAsync(user); var now = DateTime.UtcNow; var tokenOptions = new JwtSecurityToken( issuer: _configuration.Issuer, audience: _configuration.Audience, claims: claims, notBefore: now, expires: now.AddMinutes(_configuration.AccessTokenExpirationMinutes), signingCredentials: signingCredentials); return new JwtSecurityTokenHandler().WriteToken(tokenOptions); } private async Task<List<Claim>> getClaimsAsync(ApplicationUser user) { string issuer = _configuration.Issuer; var claims = new List<Claim> { // Issuer new Claim(JwtRegisteredClaimNames.Iss, issuer, ClaimValueTypes.String, issuer), // Issued at new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64, issuer), new Claim(ClaimTypes.Name, user.Email, ClaimValueTypes.String, issuer), new Claim(ClaimTypes.Email, user.Email, ClaimValueTypes.String, issuer), new Claim("Id", user.Id, ClaimValueTypes.String, issuer), new Claim("DisplayName", user.Name, ClaimValueTypes.String, issuer), }; var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role, ClaimValueTypes.String, issuer)); } return claims; } } }
در آخر، این سرویس را به صورت زیر به لیست سرویسهای ثبت شدهی پروژهی Web API، اضافه میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.AddScoped<ITokenFactoryService, TokenFactoryService>(); // ...
تکمیل کنترلر Account جهت لاگین کاربران
پس از ثبت نام کاربران، اکنون میخواهیم امکان لاگین آنها را نیز فراهم کنیم:
namespace BlazorWasm.WebApi.Controllers { [Route("api/[controller]/[action]")] [ApiController] [Authorize] public class AccountController : ControllerBase { private readonly SignInManager<ApplicationUser> _signInManager; private readonly UserManager<ApplicationUser> _userManager; private readonly ITokenFactoryService _tokenFactoryService; public AccountController( SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, ITokenFactoryService tokenFactoryService) { _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager)); _tokenFactoryService = tokenFactoryService; } [HttpPost] [AllowAnonymous] public async Task<IActionResult> SignIn([FromBody] AuthenticationDTO authenticationDTO) { var result = await _signInManager.PasswordSignInAsync( authenticationDTO.UserName, authenticationDTO.Password, isPersistent: false, lockoutOnFailure: false); if (!result.Succeeded) { return Unauthorized(new AuthenticationResponseDTO { IsAuthSuccessful = false, ErrorMessage = "Invalid Authentication" }); } var user = await _userManager.FindByNameAsync(authenticationDTO.UserName); if (user == null) { return Unauthorized(new AuthenticationResponseDTO { IsAuthSuccessful = false, ErrorMessage = "Invalid Authentication" }); } var token = await _tokenFactoryService.CreateJwtTokensAsync(user); return Ok(new AuthenticationResponseDTO { IsAuthSuccessful = true, Token = token, UserDTO = new UserDTO { Name = user.Name, Id = user.Id, Email = user.Email, PhoneNo = user.PhoneNumber } }); } } }
تا اینجا اگر برنامه را اجرا کنیم، میتوان در قسمت ورود به سیستم، برای نمونه مشخصات کاربر ادمین را وارد کرد:
و پس از اجرای درخواست، به خروجی زیر میرسیم:
که در اینجا JWT تولید شدهی به همراه قسمتی از مشخصات کاربر، در خروجی نهایی مشخص است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که به این خروجی میرسیم و حاوی claims تعریف شدهاست:
{ "iss": "https://localhost:5001/", "iat": 1616396383, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir", "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c", "DisplayName": "vahid@dntips.ir", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1616396383, "exp": 1616397583, "aud": "Any" }
تنظیم Web API برای پذیرش و پردازش JWT ها
تا اینجا پس از لاگین، یک JWT را در اختیار کلاینت قرار میدهیم. اما اگر کلاینت این JWT را به سمت سرور ارسال کند، اتفاق خاصی رخ نخواهد داد و توسط آن، شیء User قابل دسترسی در یک اکشن متد، به صورت خودکار تشکیل نمیشود. برای رفع این مشکل، ابتدا بستهی جدید نیوگت Microsoft.AspNetCore.Authentication.JwtBearer را به پروژهی Web API اضافه میکنیم، سپس به کلاس آغازین پروژهی Web API مراجعه کرده و آنرا به صورت زیر تکمیل میکنیم:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { var bearerTokensSection = Configuration.GetSection("BearerTokens"); services.AddOptions<BearerTokensOptions>().Bind(bearerTokensSection); // ... var apiSettings = bearerTokensSection.Get<BearerTokensOptions>(); var key = Encoding.UTF8.GetBytes(apiSettings.Key); services.AddAuthentication(opt => { opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(cfg => { cfg.RequireHttpsMetadata = false; cfg.SaveToken = true; cfg.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateAudience = true, ValidateIssuer = true, ValidAudience = apiSettings.Audience, ValidIssuer = apiSettings.Issuer, ClockSkew = TimeSpan.Zero, ValidateLifetime = true }; }); // ...
افزودن JWT به تنظیمات Swagger
هر کدام از اکشن متدهای کنترلرهای Web API برنامه که مزین به فیلتر Authorize باشد، در Swagger UI با یک قفل نمایش داده میشود. در این حالت میتوان این UI را به نحو زیر سفارشی سازی کرد تا بتواند JWT را دریافت و به سمت سرور ارسال کند:
namespace BlazorWasm.WebApi { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "BlazorWasm.WebApi", Version = "v1" }); c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter the token in the field", Name = "Authorization", Type = SecuritySchemeType.ApiKey }); c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, new string[] { } } }); }); } // ...
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-25.zip
We finished week 2 of the 9-week boot camp. This week was AngularJS week. We covered building the front-end of a Single Page App with the AngularJS framework. In particular, we covered topics such as client-side routing, making Ajax calls using the $http service and the $route factory, building custom AngularJS services, working with Google Maps, using Angular UI Bootstrap, and uploading files to services such as FilePicker.io.
As part of our .NET unification, Xamarin.iOS and Xamarin.Android will become part of .NET 6 as .NET for iOS and .NET for Android. Because these bindings are projections of the SDKs shipped from Apple and Google, nothing changes there, however build tooling, target framework monikers, and runtime framework monikers will be updated to match all other .NET 6 workloads. Our commitment to keeping .NET developers up-to-date with the latest mobile SDKs is foundational to .NET MAUI and remains firm. When .NET 6 ships, we expect to ship a final release of Xamarin SDKs in their current form that will be serviced for a year. All modern work will at that time shift to .NET 6.
نتایج نظرسنجی State of JS 2020
23,765 people from 137 countries took part in the recent State of JS survey and while there are some common criticisms of the project, the results are nonetheless interesting and we’ll be digging into some in forthcoming issues. Standouts include:
- Svelte took the top frontend framework crown from React for developer satisfaction.
- Testing Library jumped straight to #1 for testing libraries.
- More developers than ever are producing PWAs or using WebAssembly.
- 86% of respondents are using VS Code to work on their code.
مقایسهای بین TypeScript و ReScript
ReScript is not new. In a way it’s as old as JavaScript itself. ReScript is a rebranding of ReasonML (Facebook) and BuckleScript (Bloomberg), which wrap OCaml on both ends. The former is an interface of the OCaml syntax, while the latter makes sure to compile the AST into JavaScript. ReasonML was created by Jordan Walke, the creator of React. ReasonML still exists as a parallel project to ReScript, with a slightly different syntax and mission.
تعریف نیازمندیهای اپلیکیشن
- تنها کاربران احراز هویت شده قادر خواهند بود تا لیست ToDoهای خود را ببینند، آیتمهای جدید ثبت کنند یا دادههای قبلی را ویرایش و حذف کنند.
- کاربران نباید آیتمهای ایجاد شده توسط دیگر کاربران را ببینند.
- تنها کاربرانی که به نقش Admin تعلق دارند باید بتوانند تمام ToDoهای ایجاد شده را ببینند.
Database.SetInitializer<MyDbContext>(new MyDbInitializer());
ایجاد نقش مدیر و کاربر جدیدی که به این نقش تعلق دارد
public class MyDbInitializer : DropCreateDatabaseAlways<MyDbContext> { protected override void Seed(MyDbContext context) { var UserManager = new UserManager<MyUser>(new UserStore<MyUser>(context)); var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(context)); string name = "Admin"; string password = "123456"; //Create Role Admin if it does not exist if (!RoleManager.RoleExists(name)) { var roleresult = RoleManager.Create(new IdentityRole(name)); } //Create User=Admin with password=123456 var user = new MyUser(); user.UserName = name; var adminresult = UserManager.Create(user, password); //Add User Admin to Role Admin if (adminresult.Succeeded) { var result = UserManager.AddToRole(user.Id, name); } base.Seed(context); } }
public class MyUser : IdentityUser { public string HomeTown { get; set; } public virtual ICollection<ToDo> ToDoes { get; set; } } public class ToDo { public int Id { get; set; } public string Description { get; set; } public bool IsDone { get; set; } public virtual MyUser User { get; set; } }
public class MyDbContext : IdentityDbContext<MyUser> { public MyDbContext() : base("DefaultConnection") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { public System.Data.Entity.DbSet<AspnetIdentitySample.Models.ToDo> ToDoes { get; set; } }
تنها کاربران احراز هویت شده باید قادر به اجرای عملیات CRUD باشند
[Authorize] public class ToDoController : Controller
private MyDbContext db; private UserManager<MyUser> manager; public ToDoController() { db = new MyDbContext(); manager = new UserManager<MyUser>(new UserStore<MyUser>(db)); }
اکشن متد Create را بروز رسانی کنید
public async Task<ActionResult> Create ([Bind(Include="Id,Description,IsDone")] ToDo todo) { var currentUser = await manager.FindByIdAsync (User.Identity.GetUserId()); if (ModelState.IsValid) { todo.User = currentUser; db.ToDoes.Add(todo); await db.SaveChangesAsync(); return RedirectToAction("Index"); } return View(todo); }
اکشن متد List را بروز رسانی کنید
public ActionResult Index() { var currentUser = manager.FindById(User.Identity.GetUserId()); return View(db.ToDoes.ToList().Where( todo => todo.User.Id == currentUser.Id)); }
تنها مدیران سایت باید بتوانند تمام ToDoها را ببینند
[Authorize(Roles="Admin")] public async Task<ActionResult> All() { return View(await db.ToDoes.ToListAsync()); }
نمایش جزئیات کاربران از جدول ToDo ها
@model IEnumerable<AspnetIdentitySample.Models.ToDo> @{ ViewBag.Title = "Index"; } <h2>List of ToDoes for all Users</h2> <p> Notice that we can see the User info (UserName) and profile info such as HomeTown for the user as well. This was possible because we associated the User object with a ToDo object and hence we can get this rich behavior. 12: </p> <table class="table"> <tr> <th> @Html.DisplayNameFor(model => model.Description) </th> <th> @Html.DisplayNameFor(model => model.IsDone) </th> <th>@Html.DisplayNameFor(model => model.User.UserName)</th> <th>@Html.DisplayNameFor(model => model.User.HomeTown)</th> </tr> 25: 26: @foreach (var item in Model) 27: { 28: <tr> 29: <td> 30: @Html.DisplayFor(modelItem => item.Description) 31: </td> 32: <td> @Html.DisplayFor(modelItem => item.IsDone) </td> <td> @Html.DisplayFor(modelItem => item.User.UserName) </td> <td> @Html.DisplayFor(modelItem => item.User.HomeTown) </td> </tr> } </table>
صفحه Layout را بروز رسانی کنید تا به ToDoها لینک شود
<li>@Html.ActionLink("ToDo", "Index", "ToDo")</li> <li>@Html.ActionLink("ToDo for User In Role Admin", "All", "ToDo")</li>
ساخت یک ToDo بعنوان کاربر عادی
پس از ساختن یک ToDo میتوانید لیست رکوردهای خود را مشاهده کنید. دقت داشته باشید که رکوردهایی که کاربران دیگر ثبت کرده اند برای شما نمایش داده نخواهند شد.
مشاهده تمام ToDoها بعنوان مدیر سایت
User = Admin Password = 123456
ایجاد برنامههای ابتدایی مورد نیاز
در ابتدا دو پوشهی جدید BlazorServerApp و WinFormsApp را ایجاد میکنیم. سپس از طریق خط فرمان در اولی دستور dotnet new blazorserver و در دومی دستور dotnet new winforms را اجرا میکنیم تا دو برنامهی خالی Blazor Server و همچنین Windows Forms، ایجاد شوند. برنامهی WinForms ایجاد شده مبتنی بر NET Core. و یا همان NET 6x. است؛ بجای اینکه مبتنی بر دات نت فریمورک 4x باشد.
ایجاد یک پروژهی کتابخانهی Razor
چون میخواهیم کدهای برنامهی BlazorServerApp ما در برنامهی WinForms قابل استفاده باشد، نیاز است فایلهای اصلی آنرا به یک پروژهی razor class library منتقل کنیم. به همین جهت برای این پروژه، یک پوشهی جدید را به نام BlazorClassLibrary ایجاد کرده و درون آن دستور dotnet new razorclasslib را اجرا میکنیم.
انتقال فایلهای پروژهی Blazor به پروژهی کتابخانهی Razor
در ادامه این فایلها را از پروژهی BlazorServerApp به پروژهی BlazorClassLibrary منتقل میکنیم:
- کل پوشهی Data
- کل پوشهی Pages
- کل پوشهی Shared
- فایل App.razor
- فایل Imports.razor_
- کل پوشهی wwwroot
پس از اینکار، نیاز است فایل csproj کتابخانهی class lib را اندکی ویرایش کرد تا بتواند فایلهای اضافه شده را کامپایل کند:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <PropertyGroup> <AddRazorSupportForMvc>true</AddRazorSupportForMvc> </PropertyGroup> <ItemGroup> <FrameworkReference Include="Microsoft.AspNetCore.App" /> </ItemGroup> </Project>
- به علاوه فایل Error.cshtml.cs انتقالی، نیاز به افزودن فضای نام using Microsoft.Extensions.Logging را خواهد داشت.
- در فایل Imports.razor_ انتقالی نیاز است دو using آخر آنرا که به BlazorServerApp قبلی اشاره میکنند، به BlazorClassLibrary جدید ویرایش کنیم:
@using BlazorClassLibrary @using BlazorClassLibrary.Shared
@namespace BlazorClassLibrary.Pages
<link rel="stylesheet" href="_content/BlazorClassLibrary/css/bootstrap/bootstrap.min.css" /> <link href="_content/BlazorClassLibrary/css/site.css" rel="stylesheet" />
پس از این تغییرات، برای اینکه برنامهی BlazorServerApp موجود، به کار خود ادامه دهد، نیاز است ارجاعی از پروژهی class lib را به فایل csproj آن اضافه کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
ویرایش برنامهی WinForms جهت اجرای کدهای Blazor
تا اینجا برنامهی Blazor Server ما تمام فایلهای مورد نیاز خود را از BlazorClassLibrary دریافت میکند و بدون مشکل اجرا میشود. در ادامه میخواهیم کار هاست این class lib را در برنامهی WinForms نیز انجام دهیم. به همین جهت در ابتدا ارجاعی را به class lib به آن اضافه میکنیم:
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> </Project>
<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> </Project>
در ادامه نیاز است فایل Form1.Designer.cs را به صورت دستی جهت افزودن این WebView اضافه شده، تغییر داد:
namespace WinFormsApp; partial class Form1 { private void InitializeComponent() { this.blazorWebView1 = new Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView(); this.SuspendLayout(); this.blazorWebView1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.blazorWebView1.Location = new System.Drawing.Point(13, 181); this.blazorWebView1.Name = "blazorWebView1"; this.blazorWebView1.Size = new System.Drawing.Size(775, 257); this.blazorWebView1.TabIndex = 20; this.Controls.Add(this.blazorWebView1); this.components = new System.ComponentModel.Container(); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(800, 450); this.Text = "Form1"; this.ResumeLayout(false); } private Microsoft.AspNetCore.Components.WebView.WindowsForms.BlazorWebView blazorWebView1; }
هاست برنامهی Blazor در برنامهی WinForm
پس از تغییرات فوق، نیاز است فایلهای wwwroot را از پروژهی class lib به پروژهی WinForms کپی کرد. از این جهت که این فایلها از طریق index.html جدیدی خوانده خواهند شد. پس از کپی کردن این پوشه، نیاز است فایل csproj پروژهی WinForm را به صورت زیر اصلاح کرد:
<Project Sdk="Microsoft.NET.Sdk.Razor"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="6.0.101-preview.11.2349" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\BlazorClassLibrary\BlazorClassLibrary.csproj" /> </ItemGroup> <ItemGroup> <Content Update="wwwroot\**"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> </ItemGroup> </Project>
در ادامه داخل این پوشهی wwwroot که از پروژهی class lib کپی کردیم، نیاز است فایل index.html جدیدی را که قرار است blazor.webview.js را اجرا کند، به صورت زیر ایجاد کنیم:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Blazor WinForms app</title> <base href="/" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> <link href="WinFormsApp.styles.css" rel="stylesheet" /> </head> <body> <div id="app"></div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="">Reload</a> <a>🗙</a> </div> <script src="_framework/blazor.webview.js"></script> </body> </html>
- همچنین در این فایل باید مداخل css.های مورد نیاز را هم مجددا ذکر کرد.
مرحلهی آخر کار، استفاده از کامپوننت webview جهت نمایش فایل index.html فوق است:
using System; using System.Windows.Forms; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebView.WindowsForms; using Microsoft.Extensions.DependencyInjection; using BlazorServerApp.Data; using BlazorClassLibrary; namespace WinFormsApp; public partial class Form1 : Form { private readonly AppState _appState = new(); public Form1() { var serviceCollection = new ServiceCollection(); serviceCollection.AddBlazorWebView(); serviceCollection.AddSingleton<AppState>(_appState); serviceCollection.AddSingleton<WeatherForecastService>(); InitializeComponent(); blazorWebView1.HostPage = @"wwwroot\index.html"; blazorWebView1.Services = serviceCollection.BuildServiceProvider(); blazorWebView1.RootComponents.Add<App>("#app"); //blazorWebView1.Dock = DockStyle.Fill; } }
نکتهی مهم! حتما نیاز است WebView2 Runtime را جداگانه دریافت و نصب کرد. در غیر اینصورت در حین اجرای برنامه، با خطای نامفهوم زیر مواجه خواهید شد:
System.IO.FileNotFoundException: The system cannot find the file specified. (0x80070002)
در اینجا یک ServiceCollection را ایجاد کرده و توسط آن سرویسهای مورد نیاز کامپوننت WebView را تامین میکنیم. همچنین مسیر فایل index.html نیز توسط آن مشخص شدهاست. این تنظیمات شبیه به فایل Program.cs برنامهی Blazor هستند.
تا اینجا اگر برنامه را اجرا کنیم، چنین خروجی قابل مشاهدهاست:
اکنون برنامهی کامل Blazor Server ما توسط یک WinForms هاست شدهاست و کاربر برای کار با آن، نیاز به نصب IIS یا هیچ وب سرور خاصی ندارد.
تعامل بین برنامهی WinForm و برنامهی Blazor
میخواهیم یک دکمه را بر روی WinForm قرار داده و با کلیک بر روی آن، مقدار شمارشگر حاصل در برنامهی Blazor را نمایش دهیم؛ مانند تصویر فوق.
برای اینکار در کدهای فوق، ثبت سرویس جدید AppState را هم مشاهده میکنید:
serviceCollection.AddSingleton<AppState>(_appState);
namespace BlazorServerApp.Data; public class AppState { public int Counter { get; set; } }
builder.Services.AddSingleton<AppState>();
@inject BlazorServerApp.Data.AppState AppState // ... @code { private void IncrementCount() { // ... AppState.Counter++; } }
private void button1_Click(object sender, EventArgs e) { MessageBox.Show( owner: this, text: $"Current counter value is: {_appState.Counter}", caption: "Counter"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorDesktopHybrid.zip