تبدیل یک View به رشته و بازگشت آن به همراه نتایج JSON حاصل از یک عملیات Ajax ایی در ASP.NET MVC
ممکن است بخواهیم در پاسخ یک تقاضای Ajax ایی، اگر عملیات در سمت سرور با موفقیت انجام شد، خروجی یک Controller action را به کاربر نهایی نشان دهیم. در
چنین سناریویی لازم است که بتوانیم خروجی یک action را
بصورت رشته برگردانیم. در این مقاله به این مسئله خواهیم پرداخت .
فرض کنید در یک سیستم وبلاگ ساده قصد داریم امکان کامنت گذاشتن بصورت Ajax را پیاده سازی کنیم. یک ایده عملی و کارآ
این است: بعد از اینکه کاربر متن کامنت را وارد کرد و دکمهی ارسال کامنت را زد،
تقاضا به سمت سرور ارسال شود و اگر سرور پیغام موفقیت را صادر کرد، متن نوشته شده
توسط کاربر را به کمک کدهای JavaScript و در همان سمت کلاینت بصورت یک
کادر کامنت جدید به محتوای صفحه اضافه کنیم. بنده در اینجا برای اینکه بتوانم اصل
موضوع مورد بحث را توضیح دهم، از یک سناریوی جایگزین استفاده میکنم؛ کاربر موقعیکه دکمه ارسال را زد، تقاضا به سرور ارسال میشود. سرور بعد از انجام عملیات، تحت یک
شی JSON هم نتیجهی انجام عملیات و هم
محتوای HTML نمایش کامنت جدید در صفحه را به سمت کلاینت
ارسال خواهد کرد و کلاینت در صورت موفقیت آمیز بودن عملیات، آن محتوا را به صفحه
اضافه میکند.
با توجه به توضیحات داده شده، ابتدا یک شیء نیاز داریم تا بتوانیم توسط آن نتیجهی عملیات Ajax ایی را بصورت JSON به سمت کلاینت ارسال کنیم:
public class MyJsonResult { public bool success { set; get; } public bool HasWarning { set; get; } public string WarningMessage { set; get; } public int errorcode { set; get; }
public string message {set; get; } public object data { set; get; } }
سپس به متدی نیاز داریم که کار تبدیل نتیجهی action را به رشته، انجام دهد:
public static string RenderViewToString(ControllerContext context, string viewPath, object model = null, bool partial = false) { ViewEngineResult viewEngineResult = null; if (partial) viewEngineResult = ViewEngines.Engines.FindPartialView(context, viewPath); else viewEngineResult = ViewEngines.Engines.FindView(context, viewPath, null); if (viewEngineResult == null) throw new FileNotFoundException("View cannot be found."); var view = viewEngineResult.View; context.Controller.ViewData.Model = model; string result = null; using(var sw = new StringWriter()) { var ctx = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); view.Render(ctx, sw); result = sw.ToString(); } return result; }
فرض کنیم در سمت Controller هم از کدی شبیه به این استفاده میکنیم:
public JsonResult AddComment(CommentViewModel model) { MyJsonResult result = new MyJsonResult() { success = false; }; if (!ModelState.IsValid) { result.success = false; result.message = "لطفاً اطلاعات فرم را کامل وارد کنید"; return Json(result); } try { Comment theComment = model.toCommentModel(); //EF service factory Factory.CommentService.Create(theComment); Factory.SaveChanges(); result.data = Tools.RenderViewToString(this.ControllerContext, "/views/posts/_AComment", model, true); result.success = true; } catch (Exception ex) { result.success = false; result.message = "اشکال زمان اجرا"; } return Json(result); }
و در سمت کلاینت برای ارسال Form به صورت Ajax ایی خواهیم داشت:
@using (Ajax.BeginForm("AddComment", "posts", new AjaxOptions() { HttpMethod = "Post", OnSuccess = "AddCommentSuccess", LoadingElementId = "AddCommentLoading" }, new { id = "frmAddComment", @class = "form-horizontal" })) { @Html.HiddenFor(m => m.PostId) <label for="fname">@Texts.ContactName</label> <input type="text" id="fname" name="FullName" class="form-control" placeholder="@Texts.ContactName "> <label for="email">@Texts.Email</label> <input type="email" id="InputEmail" name="email" class="form-control" placeholder="@Texts.Email"> <br><textarea name="C_Content" cols="60" rows="10" class="form-control"></textarea><br> <input type="submit" value="@Texts.SubmitComments" name="" class="btn btn-primary"> <div class="loading-mask" style="display:none">@Texts.LoadingMessage</div> }
باید توجه شود Texts در اینجا یک Resource هست که به منظور نگهداری کلمات استفاده شده در سایت، برای زبانهای مختلف استفاده میشود (رجوع شود به مفهوم بومی سازی در Asp.net) .
و در قسمت script ها داریم:
<script type="text/javascript"> function AddCommentSuccess(jsData) { if (jsData && jsData.message) alert(jsData.message); if (jsData && jsData.success) { document.getElementById("frmAddComment").reset(); //افزودن کامنت جدید ساخته شده توسط کاربر به لیست کامنتهای صفحه $("#divAllComments").html(jsData.data + $("#divAllComments").html()); } } </script>
public class OrderItem { public int OrderItemID { get; set; } public int Quantity { get; set; } public string ItemCode { get; set; } public string Description { get; set; } } public class Order { public Order() { Items = new List<OrderItem>(); } public int OrderID { get; set; } public string Description { get; set; } public decimal TotalWeight { get; set; } public string ShippingMethod { get; set; } public List<OrderItem> Items { get; set; } }
OrderInfo.ShippingMethod
TotalAmount + (OrderInfo.TotalWeight * 0.50m)
static void Main(string[] args) { Order myOrder = new Order { OrderID = 1, Description = "Need some stuff", ShippingMethod = "2ndDay", TotalWeight = 100 }; IDictionary<String, object> input = new Dictionary<String, Object> { { "OrderInfo",myOrder} }; IDictionary<String, Object> output = WorkflowInvoker.Invoke(new OrderWF(), input); Decimal total = (Decimal)output["TotalAmount"]; Console.WriteLine("Workflow returned ${0} for my order total", total); Console.WriteLine("Press ENTER to exit"); Console.ReadLine(); //Activity workflow1 = new OrderWF(); //WorkflowInvoker.Invoke(workflow1); }
مقدمه ای بر AutoMapper
install-package automapper
Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection
using AutoMapper;
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAutoMapper(); }
public class UserMappingProfile:Profile { public UserMappingProfile() { CreateMap<User, UserPost>(); CreateMap<UserPost,User >() .ForMember(dest=>dest.Username,src=>src.Ignore()) .ForMember(dest=>dest.Email,src=>src.Ignore()); } }
private readonly IMapper _mapper; public UserController(IMapper mapper) { _mapper = mapper; } public async Task<UserPost> FindUser(string username) { var users = await _userServices.GetUser(username); var user = _mapper.Map<User,UserPost>(users); return user; }
import { Component, Input, OnInit } from '@angular/core'; import { AbstractControl } from '@angular/forms'; @Component({ selector: 'validation-message', template: ` <ng-container *ngIf="control.invalid && control.touched"> {{ message }} </ng-container> ` }) export class ValidationMessageComponent implements OnInit { @Input() control: AbstractControl; @Input() fieldDisplayName: string; @Input() rules: { [key: string]: string }; get message(): string { return this.control.hasError('required') ? `${this.fieldDisplayName} را وارد نمائید.` : this.control.hasError('pattern') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('email') ? `${this.fieldDisplayName} را به شکل صحیح وارد نمائید.` : this.control.hasError('minlength') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.minlength.requiredLength } کاراکتر باشد.` : this.control.hasError('maxlength') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.maxlength.requiredLength } کاراکتر باشد.` : this.control.hasError('min') ? `${this.fieldDisplayName} باید بیشتر از ${ this.control.errors.min.requiredLength } باشد.` : this.control.hasError('max') ? `${this.fieldDisplayName} باید کمتر از ${ this.control.errors.max.requiredLength } باشد.` : this.hasRule() ? this.findRule() : this.control.hasError('model') ? `${this.control.errors.model.messages[0]}` : ''; } constructor() {} private hasRule() { return ( this.rules && Object.keys(this.control.errors).some(ruleKey => this.rules[ruleKey] ? true : false ) ); } private findRule(): string { let message = ''; Object.keys(this.control.errors).forEach(ruleKey => { if (this.rules[ruleKey]) { message += `${this.rules[ruleKey]} `; } }); return message; } ngOnInit(): void {} }
<mat-error *ngIf="form.controls['userName'].invalid && form.controls['userName'].touched" class="mat-text-warn"> <validation-message [control]="form.controls['userName']" fieldDisplayName="نام کاربری" [rules]="{rule1:'پیغام متناظر با rule1'}"> </validation-message> </mat-error>
this.form = this.formBuilder.group({ userName: [ '', [Validators.required, UserNameValidators.rule1)] ], password: ['', Validators.required], rememberMe: [false] }); export class UserNameValidators{ static rule1(control: AbstractControl) { if (control.value.indexOf(' ') >= 0) { return { rule1: true }; } return null; } }
- آشنایی با انواع Control IDها در ASP.Net
مانند:
$('#<%= TextBox1.ClientID %>')
<asp:TextBox runat="server" ID="txtName" ClientIDMode="Static" />
انواع رخدادها: بومی و سفارشی
دو رده بندی عمومی رخدادها در مرورگرها وجود دارند: بومی و سفارشی.
بومیها همانهایی هستند که در مستندات رسمی استانداردهای وب ذکر شدهاند؛ مانند click که توسط ماوس و یا صفحه کلید فعال میشود و یا load که در زمان بارگذاری کامل صفحه، تصاویر و یا یک iframe رخ میدهد.
رخدادهای سفارشی مواردی هستند که توسط یک کتابخانهی خاص و یا جهت یک برنامهی خاص تهیه شدهاند. مانند یک رخداد سفارشی که زمان شروع آپلود یک فایل را اعلام میکند.
رخدادهای سفارشی که بدون jQuery ایجاد و رخمیدهند، توسط jQuery نیز قابل بررسی و مدیریت هستند و نه برعکس. به عبارتی رخدادهای سفارشی ایجاد شدهی توسط jQuery غیراستاندارد بوده و صرفا مختص به API آن هستند.
در این بین، شیء استاندارد Event کار اتصال رخدادهای سفارشی و استاندارد را انجام میدهد. هر نوع رخداد DOM (سفارشی و یا بومی)، توسط یک شیء Event بیان میشود که آن نیز به همراه تعدادی خاصیت و متد، جهت مدیریت این رخداد است. برای مثال رخداد click دارای خاصیت type ایی به نام click است که در شیء Event متناظر با آن تعریف شدهاست.
انتشار رخدادها در صفحه
در روزهای آغازین وب، Netscape روش event capturing را برای انتشار رخدادها در صفحه ارائه داد و در مقابل آن IE روش event bubbling را معرفی کرد که متضاد یکدیگر بودند. در سال 2000 با ارائه استاندارد DOM Level 2 Events Specification، این وضعیت تغییر کرد و شامل هر دو مورد event capturing و event bubbling است و در حال حاضر تمام مرورگرهای مدرن این استاندارد را پیاده سازی کردهاند. بر اساس این استاندارد، زمانیکه رویدادی خلق میشود، فاز capturing آغاز میگردد که از شیء window شروع، سپس به شیء document منتشر میشود و این روند تا رسیدن به المانی که سبب بروز رخداد شدهاست ادامه پیدا میکند. پس از پایان فاز capturing، فاز جدید bubbling شروع میشود. در این فاز، رخداد از تمام والدین شیء هدف عبور میکند تا به شیء window برسد.
برای مثال اگر سند HTML ما چنین تعریفی را داشته باشد و بر روی المان «child of child of one» کلیک شده باشد:
<!DOCTYPE html> <html> <head> <title>event propagation demo</title> </head> <body> <section> <h1>nested divs</h1> <div>one <div>child of one <div>child of child of one</div> </div> </div> </section> </body> </html>
1.window 2.document 3.<html> 4.<body> 5.<section> 6.<div>one 7.<div>child of one 8.<div>child of child of one
9.<div>child of child of one 10.<div>child of one 11.<div>one 12.<section> 13.<body> 14.<html> 15.document 16.window
البته باید درنظر داشت که jQuery از روش ارائه شدهی توسط مرورگر برای فاز Bubbling استفاده نمیکند و این مسیر را خودش مجددا محاسبه و رخدادگردانهای این مسیر را به صورت دستی اجرا میکند. به همین جهت کارآیی آن نسبت به روش توکار و بومی مرورگرها کمتر است.
ایجاد رخدادهای DOM و صدور آنها در jQuery
برای نمایش ایجاد و صدور رخدادهای DOM با و بدون jQuery، از قطعه کد HTML زیر استفاده میکنیم:
<div> <button type="button">do something</button> </div> <form method="POST" action="/user"> <label>Enter user name: <input name="user"> </label> <button type="submit">submit</button> </form>
// submits the form $('FORM').trigger('submit'); // submits the form by clicking the button $('BUTTON[type="submit"]').trigger('click'); // focuses the text input $('INPUT').trigger('focus'); // removes focus from the text input $('INPUT').trigger('blur');
هرچند روش دومی نیز در jQuery API برای انجام همینکارها نیز پیش بینی شدهاست:
// submits the form $('FORM').submit(); // submits the form by clicking the button $('BUTTON[type="submit"]').click(); // focuses the text input $('INPUT').focus(); // removes focus from the text input $('INPUT').blur();
در ادامه فرض کنید یک دکمه داخل یک div قرار گرفتهاست و آن div نیز به همراه یک مدیریت کنندهی رخداد کلیک است. در این حالت اگر بخواهیم با کلیک بر روی دکمه سبب اجرای رویدادگردان div والد نشویم، میتوان از متد triggerHandler استفاده کرد:
// clicks the first button - the click event does not bubble $('BUTTON[type="button"]').triggerHandler('click');
ایجاد رخدادهای DOM و صدور آنها در جاوا اسکریپت (بدون استفاده از jQuery)
در web API مرورگرها، برای انجام بروز رخدادهای معادل مثالی که با jQuery مطرح شد، میتوان متدهای بومی متناظر با این رخدادها را بر روی المانها فراخوانی کرد:
// submits the form document.querySelector('FORM').submit(); // submits the form by clicking the button document.querySelector('BUTTON[type="submit"]').click(); // focuses the text input document.querySelector('INPUT').focus(); // removes focus from the text input document.querySelector('INPUT').blur();
متدهای توکار و بومی click ،focus و blur بر روی تمام عناصر DOM که از اینترفیس HTMLElement مشتق شده باشند، وجود دارند. متد submit فقط بر روی المانهایی از نوع <form> وجود دارد و قابل فراخوانی است.
باید دقت داشت که فراخوانی متدهای click و submit از نوع bubbling است؛ اما متدهای focus و blur خیر. از این جهت که این دو رخداد فاز capturing را سبب میشوند.
متدهای یاد شده را توسط سازندهی شیء Event و یا متد createEvent شیء document نیز میتوان ایجاد کرد. یکی از کاربردهای آن، ارائهی رفتاری سفارشی مانند triggerHandler جیکوئری است:
var clickEvent; if (typeof Event === 'function') { clickEvent = new Event('click', {bubbles: false}); } else { clickEvent = document.createEvent('Event'); clickEvent.initEvent('click', false, true); } document.querySelector('BUTTON[type="button"]').dispatchEvent(clickEvent);
ایجاد و صدور رخدادهای سفارشی
فرض کنید در حال تهیهی کتابخانهای هستیم که افزودن و حذف آیتمها را به یک گالری عکس ارائه میدهد. میخواهیم روشی را در اختیار مصرف کننده قرار دهیم تا بتواند به این رخدادهای سفارشی (غیر استانداردی که جزو W3C نیستند) گوش فرا دهد.
در جیکوئری برای ایجاد رخدادهای سفارشی به صورت زیر عمل میشود:
// Triggers a custom "image-removed" element, // which bubbles up to ancestor elements. $libraryElement.trigger('image-removed', {id: 1});
در خارج از جیکوئری و توسط web API استاندارد مرورگرها ایجاد و صدور رخدادهای سفارشی به همراه bubbling آن به صورت زیر است:
var event = new CustomEvent('image-removed', { bubbles: true, detail: {id: 1} }); libraryElement.dispatchEvent(event);
var event = document.createEvent('CustomEvent'); event.initCustomEvent('image-removed', false, true, {id: 1}); libraryElement.dispatchEvent(event);
var event; // If the `CustomEvent` constructor function is not supported, // fall back to `createEvent` method. if (typeof CustomEvent === 'function') { event = new CustomEvent('image-removed', { bubbles: true, detail: {id: 1} }); } else { event = document.createEvent('CustomEvent'); event.initCustomEvent('image-removed', false, true, { id: 1 }); } libraryElement.dispatchEvent(event);
گوش فرادادن به رخدادهای صادر شده، توسط jQuery
در جیکوئری با استفاده از متد on آن میتوان به تمام رخدادهای استاندارد و همچنین سفارشی گوش فرا داد:
$(window).on('resize', function() { // react to new window size });
// remove all resize listeners - usually a bad idea $(window).off('resize');
روش بهتر انجام اینکار، ذخیرهی ارجاعی به متدی است که قرار است این رویداد گردانی را انجام دهد:
var resizeHandler = function() { // react to new window size }; $(window).on('resize', resizeHandler); // ...later // remove only our resize handler $(window).off('resize', resizeHandler);
همچنین اگر یک گوش فراهندهی به رخدادی تنها قرار است یکبار در طول عمر برنامه اجرا شود، میتوان از متد one استفاده کرد:
$(someElement).one('click', function() { // handle click event });
گوش فرادادن به رخدادهای صادر شده، توسط جاوا اسکریپت خالص (یا همان web API مرورگرها)
ابتداییترین روش گوش فرادادن به رخدادها که از زمان آغاز معرفی آنها در دسترس بودهاست، روش تعریف inline آنها است:
<button onclick="handleButtonClick()">click me</button>
روش دیگر ثبت رویدادگردان click، انتساب متد آن به خاصیت رخداد متناظری در آن المان ویژه است:
buttonEl.onclick = function() { // handle button click };
البته باید دقت داشت که یکی از دو روش یاد شده را میتوانید استفاده کنید. در اینجا آخرین رویدادگردان متصل شدهی به المان، همواره تمام نمونههای موجود دیگر را بازنویسی میکند.
اگر نیاز به معرفی رویدادگردانهای متعددی برای یک المان در ماژولهای مختلف برنامه وجود داشت، از زمان IE 9.0 به بعد، متد addEventListener برای این منظور تدارک دیده شدهاست و syntax آن بسیار شبیه به متد on جیکوئری است:
buttonEl.addEventListener('click', function() { // handle button click });
برای نمونه معادل قطعه کد جیکوئری که پیشتر با متد on نوشتیم، با جاوا اسکریپت خالص به صورت زیر است:
window.addEventListener('resize', function() { // react to new window size });
var resizeHandler = function() { // react to new window size }; window.addEventListener('resize', resizeHandler); // ...later // remove only our resize handler window.removeEventListener('resize', resizeHandler);
در اینجا حتی امکان تعریف متد one جیکوئری نیز پیش بینی شدهاست (البته جزو استانداردهای جدید وب از سال 2016 است):
someElement.addEventListener('click', function(event) { // handle click event }, { once: true });
var clickHandler = function() { // handle click event // ...then unregister handler someElement.removeEventListener('click', clickHandler); }; someElement.addEventListener('click', clickHandler);
کنترل انتشار رخدادها
فرض کنید میخواهیم جلوی انتخاب المانهای صفحه مانند تصاویر و متن را توسط ماوس بگیریم. روش انجام اینکار با jQuery به صورت زیر است:
$(window).on('mousedown', function(event) { event.preventDefault(); });
window.addEventListener('mousedown', function(event) { event.preventDefault(); });
برای جلوگیری کردن از انتشار رخدادی مانند click جهت رسیدن به سایر رویدادگردانهای ثبت شدهی در بین راه فاز bubbling، میتوان از متد stopPropagation استفاده کرد. روش انجام اینکار در جیکوئری:
$someElement.on('click', function(event) { event.stopPropagation(); });
و با web Api جهت جلوگیری از انتشار رخدادها در فاز capturing (این تنها راه مدیریت فاز capturing است):
// stop propagation during capturing phase someElement.addEventListener('click', function(event) { event.stopPropagation(); }, true);
// stop propagation during bubbling phase someElement.addEventListener('click', function(event) { event.stopPropagation(); });
$someElement.on('click', function(event) { event.stopImmediatePropagation(); });
someElement.addEventListener('click', function(event) { event.stopImmediatePropagation(); });
یک نکته: در این حالت اگر متد رویدادگردانی مقدار false را برگرداند، به معنای فراخوانی هر دوی متد preventDefault و stopPropagation است.
ارسال اطلاعات به رویدادگردانها
روش ارسال اطلاعات اضافی به رویداد گردانها در جیکوئری به صورت زیر است:
$uploaderElement.trigger('uploadError', { filename: 'picture.jpeg' });
$uploaderParent.on('uploadError', function(event, data) { showAlert('Failed to upload ' + data.filename); });
روش انجام اینکار با web API مرورگرها به صورت زیر است:
// send the failed filename w/ an error event var event = new CustomEvent('uploadError', { bubbles: true, detail: {filename: 'picture.jpeg'} }); uploaderElement.dispatchEvent(event); // ...and this is a listener for the event uploaderParent.addEventListener('uploadError', function(event) { showAlert('Failed to upload ' + event.detail.filename); });
و اگر میخواهید از IE هم پشتیبانی کنید، روش جایگزین کردن شیء CustomEvent با createEvent به صورت زیر است:
// send the failed filename w/ an error event var event = document.createEvent('CustomEvent'); event.initCustomEvent('uploadError', true, true, { filename: 'picture.jpeg' }); uploaderElement.dispatchEvent(event); // ...and this is a listener for the event uploaderParent.addEventListener('uploadError', function(event) { showAlert('Failed to upload ' + event.detail.filename); });
متوجه شدن زمان بارگذاری یک شیء در صفحه
در حین توسعهی برنامههای وب، با این نوع سؤالات زیاد مواجه خواهید شد: چه زمانی تمام و یا بعضی از المانهای صفحه کاملا بارگذاری و رندر شدهاند؟
پاسخ به این نوع سؤالات در W3C UI Events specification توسط رویداد استاندارد load داده شدهاست.
- چه زمانی تمام المانهای موجود در صفحه کاملا بارگذاری و رندر شده و همچنین شیوهنامههای تعریف شده نیز به آنها اعمال گردیدهاند؟
روش انجام اینکار با jQuery:
$(window).on('load', function() { // page is fully rendered });
window.addEventListener('load', function() { // page is fully rendered });
- چه زمانی markup استاتیک صفحهی جاری در جای خود قرار گرفتهاند؟
اهمیت این موضوع، به دسترسی به زمان مناسب و امن ایجاد تغییرات در DOM بر میگردد. برای این منظور رویداد استاندارد DOMContentLoaded پیشبینی شدهاست که زودتر از رویداد load، در دسترس برنامه نویس قرار میگیرد. در جیکوئری توسط یکی از دو روش معروف زیر به رویداد یاد شده دسترسی خواهید داشت:
$(document).ready(function() { // markup is on the page }); //or $(function() { // markup is on the page });
document.addEventListener('DOMContentLoaded', function() { // markup is on the page });
یک نکته: بهتر است این تعریف web API را پیش از تگهای <link> قرار دهید. زیرا بارگذاری آنها، اجرای هر نوع اسکریپتی را تا زمان پایان عملیات، سد میکند.
- چه زمانی المانی خاص در صفحه بارگذاری شدهاست و چه زمانی بارگذاری یک المان با شکست مواجه شدهاست؟
در جیکوئری توسط بررسی رویدادهای load و error میتوان به وضعیت نهایی بارگذاری المانهایی خاص دسترسی یافت:
$('IMG').on('load', function() { // image has successfully loaded }); $('IMG').on('error', function() { // image has failed to load });
document.querySelector('IMG').addEventListener('load', function() { // image has successfully loaded }); document.querySelector('IMG').addEventListener('error', function() { // image has failed to load });
- جلوگیری از ترک اتفاقی صفحهی جاری
گاهی از اوقات نیاز است برای از جلوگیری از تخریب صفحهی جاری و از دست رفتن اطلاعات ذخیره نشدهی کاربر، اگر بر روی دکمهی close بالای صفحه کلیک کرد و یا کاربر به اشتباه به صفحهی دیگری هدایت شد، جلوی اینکار را بگیریم. برای این منظور رخداد استاندارد beforeunload درنظر گرفته شدهاست. روش استفادهی از این رویداد در جیکوئری:
$(window).on('beforeunload', function() { return 'Are you sure you want to unload the page?'; });
window.addEventListener('beforeunload', function(event) { var message = 'Are you sure you want to unload the page?'; event.returnValue = message; return message; });
ایجاد برنامههای ابتدایی مورد نیاز
در ابتدا دو پوشهی جدید 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
جلوگیری از deadlock در برنامههای async
using System; using System.Net.Http; using System.Threading.Tasks; using System.Windows.Forms; using Newtonsoft.Json.Linq; namespace Async13 { public static class JsonExt { public static async Task<JObject> GetJsonAsync(this Uri uri) { using (var client = new HttpClient()) { var jsonString = await client.GetStringAsync(uri); return JObject.Parse(jsonString); } } } public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnGo_Click(object sender, EventArgs e) { var url = "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo"; txtResult.Text = new Uri(url).GetJsonAsync().Result.ToString(); } } }
PM> Install-Package Newtonsoft.Json
در اینجا قصد داریم اطلاعات JSON دریافتی را در یک TextBox نمایش دهیم. کاری که انجام شده، فراخوانی متد async ایی است به نام GetJsonAsync و سپس استفاده از خاصیت Result این Task برای صبر کردن تا پایان عملیات.
اگر برنامه را اجرا کنید و بر روی دکمهی دریافت اطلاعات کلیک نمائید، برنامه قفل خواهد کرد. چرا؟
البته تفاوتی هم نمیکند که این یک برنامهی دسکتاپ است یا یک برنامهی وب. در هر دو حالت یک deadlock کامل را مشاهده خواهید کرد.
علت بروز deadlock در کدهای async چیست؟
همواره نتیجهی await، در context فراخوان آن بازگشت داده میشود. اگر برنامهی دسکتاپ است، این context همان ترد اصلی UI برنامه میباشد و اگر برنامهی وب است، این context، زمینهی درخواست در حال پردازش میباشد.
خاصیت Result و یا استفاده از متد Wait یک Task، به صورت همزمان عمل میکنند و نه غیرهمزمان. متد GetJsonAsync یک Task ناتمام را که فراخوان آن باید جهت پایاناش صبر کند، بازگشت میدهد. سپس در همینجا کد فراخوان، تردجاری را توسط فراخوانی خاصیت Result قفل میکند. متد GetJsonAsync منتظر خواهد ایستاد تا این ترد آزاد شده و بتواند به کارش که بازگردان نتیجهی عملیات به context جاری است، ادامه دهد.
به عبارتی، کدهای async منتظر پایان کار Result هستند تا نتیجه را بازگردانند. در همین لحظه کدهای همزمان برنامه نیز منتظر کدهای async هستند تا خاتمه یابند. نتیجهی کار یک deadlock است.
روشهای جلوگیری از deadlock در کدهای async؟
الف) در مورد متد ConfigureAwait در قسمتهای قبل بحث شد و به عنوان یک best practice مطرح است:
public static class JsonExt { public static async Task<JObject> GetJsonAsync(this Uri uri) { using (var client = new HttpClient()) { var jsonString = await client.GetStringAsync(uri).ConfigureAwait(continueOnCapturedContext: false); return JObject.Parse(jsonString); } } }
ب) راه حل دوم، عدم استفاده از خواص و متدهای همزمان با متدهای غیر همزمان است:
private async void btnGo_Click(object sender, EventArgs e) { var url = "http://api.geonames.org/citiesJSON?north=44.1&south=-9.9&east=-22.4&west=55.2&lang=de&username=demo"; var data = await new Uri(url).GetJsonAsync(); txtResult.Text = data.ToString(); }
خلاصهی بحث
Await را با متدهای همزمان Wait یا خاصیت Result بلاک نکنید. در غیراینصورت در ترد اجرا کنندهی دستورات، یک deadlock رخخواهد داد؛ زیرا نتیجهی await باید به context جاری بازگشت داده شود اما این context توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شدهاست.