-
Separated the existing "Restore Packages" option in the Command Palette into two distinct functions:
- "Restore Project" - Displays a drop-down that shows all the available projects in the solution or in the workspace. Selecting one of them would trigger a dotnet restore for the particular project.
- "Restore All Projects" - Triggers a dotnet restore for all projects in the current solution or workspace.
- بررسی ویرویس Stuxnet | www.idevcenter.com
- تایپ متن فارسی در Gmail | googlepersianblog.blogspot.com
- خصوصیات جدید در ASP.NET 4.5 - خصوصیت HTML Editor Smart Tasks and Event Handler Generation | mojtabasahraei.blogfa.com
- دیتابیس رنگی | www.dotnetdev.info
- شما مرغابی هستید یا عقاب؟ | hrnews.blogfa.com
- نقد فیلم "one flew over the cuckoos nest - دیوانه از قفس پرید( پرواز بر فراز آشیانه فاخته)" | filmhafteh.blogfa.com
- Addressing Visual Studio performance | blogs.msdn.com
- BUILD conference–day 3 | lostechies.com
- BUILD: WinRT, Silverlight, WPF, XAML | www.japf.fr
- C# and Visual Basic on the WinRT API | www.infoq.com
- Multi Monitor in VS.NET 2010 & SQL Server 2011 | www.nikamooz.com
- My List of Must-See Build 2011 Videos | 10rem.net
- New CSS editor features in Visual Studio 11 Developer Preview | blogs.msdn.com
- New Tools and New Content - ASP.NET, Visual Studio 11 Web and .NET 4.5 Developer Preview (with commentary) | www.hanselman.com
- Prototype xUnit.net Visual Studio 11 Unit Testing Plugin | bradwilson.typepad.com
- RIA Services updates for //build | feeds.jeffhandley.com
- Using WinRT from .NET | ermau.com
- Windows 8 and WinRT: Links, News and Resources | ajlopez.wordpress.com
- Windows 8 Replaces the Win32 API | www.infoq.com
- Windows 8: What you Need to Know | csharperimage.jeremylikness.com
- WinRT: An Object Orientated Replacement for Win32 | www.infoq.com
در قسمت قبل کلیات نحوهی استفاده از Animation در Angular را مورد بررسی
قرار دادیم. در این بخش قصد داریم نحوهی اعمال Animation های پیشرفتهتری
را مورد بررسی قرار دهیم.
وضعیت void
این وضعیت به تمامی المانهایی که به view متصل نیستند، اعمال خواهد شد. این عدم اتصال به view برای یک المان میتواند بخاطر این باشد که این المان هنوز به صفحه وارد نشده است یا اینکه قبلا در صفحه بوده و الان در حال حذف شدن است.
درکل وضعیت void برای تعریف انیمیشنی در هنگام ورود و خروج المان به و از صفحه مورد استفاده قرار میگیرد. برای مثال گذار *=>void به تمامی المانهایی که view را ترک میکنند اعمال خواهد شد و void=>* به المانهایی که به view اضافه میشوند.
قطعه کد زیر سبب تعریف انیمیشنی بر روی المنتهای ورودی و خروجی از صفحه خواهد شد:
animations: [ trigger('flyInOut', [ transition('void => *', [ style({transform: 'translateX(-100%)'}), animate(100) ]), transition('* => void', [ animate(100, style({transform: 'translateX(100%)'})) ]) ]) ]
در این قطعه کد یک trigger به نام flyInOut تعریف شده است که در آن برای گذار ورود و خروج المنت در صفحه، انیمیشن تعریف شده است. همانطور که واضح است نیازی به تعریف حالت void، توسط تابع state وجود ندارد.
کد زیر نحوه استفاده از این trigger را نشان میدهد
(با فرض اینکه لیستی از کاربران را در متغییر users داریم که با
فراخوانی متد addNewUser یک آیتم به آن اضافه شده و با زدن دکمه Remove آیتم مورد نظر
از لیست حذف میشود):
<ul> <li *ngFor="let user of users" [@flyInOut]> {{user.FirstName}} <button (click)="remove(user)">Remove</button> </li> </ul> <button (click)="addNewUser()">Add New User</button>
همچنین به جای void=>* در تابع transition، از :enter و به جای *=>void، از :leave میتوان استفاده کرد.
transition(':enter', [ ... ]); // void => * transition(':leave', [ ... ]); // * => void
واضح است که شما میتوانید از حالت void به هر حالت
تعریف شدهی توسط خودتان نیز گذاری را تعریف کنید. برای مثال اگر قبلا حالت active و inactive را با استفاده
از تابع state
ساخته باشید، گذارهای زیر قابل تعریف خواهند بود و هیچگونه محدودیتی وجود نخواهد
داشت:
transition('void => inactive', //...) transition('inactive => void', //...) transition('void => active', //..) transition('active => void', //...)
کاربرد * در style
فرض کنید میخواهیم گذاری را تعریف کنیم که هنگام ورود
المنت، در ابتدا ارتفاع المنت را به مقداری 0px تنظیم
کرده و سپس همراه با یک انیمیشن، مقدار ارتفاع را به مقدار اصلی تنظیم خواهد کرد. چالشی
که در اینجا وجود دارد این است که مقدار ارتفاع المنت مشخص نیست و بستگی به اندازه
صفحه نمایش داشته و توسط آن css تنظیم
خواهد شد. در اینجا میتوان از * برای
بدست آورن مقدار جاری یک خصوصیت از استایل استفاده کرد:
transition('void => *', [ style({height: 0 }), animate(1000,style({ color: '*' })) ]),
انیمیشن چند مرحلهای با استفاده از Keyframes
تا اینجا تمامی انیمیشنهایی را که بررسی کردیم، یک انیمیشن یک مرحلهای بودند. در صورتیکه یک انیمیشن حرفهای، متشکل از چند مرحله گذار خواهد بود. برای انجام اینکار از تابع Keyframes استفاده میکنیم. برای مثال میخواهیم انیمیشن ورود المنت را به صورتی در نظر بگیریم که المنت در ابتدا در نقطه -75% بالاتر از مکانیکه در آنجا نمایش داده خواهد شد، با opacity صفر شروع به حرکت کرده و در مرحله بعد به نقطه 35px پائینتر از مکان اصلی خود آمده و opacity نیم را خواهد داشت و در نهایت، با حرکت بعدی به جای اصلی خود خواهد رفت و opacity یک را پیدا میکند.
animations: [ trigger('flyInOut', [ transition('void => *', [ animate(300, keyframes([ style({opacity: 0, transform: 'translateY(-75%)', offset: 0}), style({opacity: 0.5, transform: 'translateY(35px)', offset: 0.3}), style({opacity: 1, transform: 'translateY(0)', offset: 1.0}) ])) ]) ]) ]
تابع Keyframes آرایهای از تابع style را دریافت میکند که هر تابع شامل خصوصیتهای انیمیشن به همراه یه خصوصیت offset است. این خصوصیت اختیاری است و مقدار صفر تا یک را قبول میکند و بیانگر زمان اجرای تابع style بعدی است.
رخداد شروع و پایان انیمیشن
با استفاده از @triggerName.start و @triggerName.done با شروع و پایان انیمیشن خود میتوانید یک تابع سفارشی را نیز اجرا کنید. برای مثال کد زیر را در نظر بگیرید:
template: ` <ul> <li *ngFor="let hero of heroes" (@flyInOut.start)="animationStarted($event)" (@flyInOut.done)="animationDone($event)" [@flyInOut]="'in'"> {{hero.name}} </li> </ul> `,
در این مثال هنگام شروع انیمیشن تابع animationStarted و پس از اتمام انیمیشن، تابع animationDone اجرا خواهند شد.
سری آموزشی Angular Material
تا اینجا در قسمت سوم، نحوهی قراردادن یک کامپوننت را در کامپوننتی دیگر، توسط مقدار دهی خاصیت directives مزین کنندهی Component بررسی کردیم. همینقدر که یک کامپوننت دارای selector باشد، قابلیت قرارگرفتن در یک کامپوننت دیگر را دارد. اما چگونه باید بین این کامپوننتها ارتباط برقرار کرد؟
تهیه کامپوننت نمایش ستارهای امتیازهای محصولات
مثال نمایش لیست محصولات سری جاری، دارای ستون «5Star Rating» است. در این قسمت میخواهیم بجای نمایش عددی این امتیازها، کامپوننتی را طراحی کنیم که نماش ستارهای آنها را سبب شود. این کامپوننت باید بتواند یک مقدار ورودی، یا همان عدد امتیاز محصول را از کامپوننت دربرگیرندهی آن دریافت کند. همچنین میخواهیم اگر کاربر بر روی این ستارهها کلیک کرد، کامپوننت در برگیرنده را نیز مطلع سازیم.
در این مثال در فایل product-list.component.html چنین سطری تعریف شدهاست:
<td>{{ product.starRating }}</td>
با توجه به اینکه کامپوننت نمایش ستارهای امتیازها، قابلیت استفادهی مجدد را دارد و الزامی ندارد که حتما در لیست محصولات، بکار گرفته شود، بهتر است محل تعریف آنرا به خارج از پوشهی products فعلی منتقل کنیم. برای مثال میتوان پوشهی app\shared را برای آن و تمامی کامپوننتهای با قابلیت استفادهی مجدد ایجاد کرد.
برای شروع، فایل جدید App\shared\star.component.ts را اضافه کنید؛ با کدهای کامل ذیل:
import { Component, OnChanges, Input, Output, EventEmitter } from 'angular2/core'; @Component({ selector: 'ai-star', templateUrl: 'app/shared/star.component.html', styleUrls: ['app/shared/star.component.css'] }) export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>(); ngOnChanges(): void { this.starWidth = this.rating * 86 / 5; } onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); } }
سپس مسیر template و مسیر فایل css ویژهی آن، در تزئین کنندهی Component مشخص شدهاند. محتوای کامل این دو فایل را در ذیل مشاهده میکنید:
الف) محتوای فایل App\shared\star.component.html
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'> <div style="width: 86px"> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> <span class="glyphicon glyphicon-star"></span> </div> </div>
.crop { overflow: hidden; } div { cursor: pointer; }
معرفی مقدماتی life cycle hooks در قسمت قبل صورت گرفت. در اینجا چون نیاز است به ازای هر بار رندر شدن این کامپوننت، عرض آن متفاوت باشد، بنابراین نیاز است راهی را پیدا کنیم تا بتوان مقدار خاصیت starWidth را متغیر کرد. به همین منظور از hook مخصوص این تغییرات یا همان OnChanges استفاده میشود. بنابراین باید کلاس این کامپوننت، اینترفیس OnChanges را پیاده سازی کند. پس از آن، importهای لازم جهت تعریف OnChanges به ابتدای فایل اضافه شده و همچنین متد ngOnChanges نیز جهت تکمیل کار پیاده سازی اینترفیس OnChanges، به کلاس جاری اضافه میشود.
کار متد ngOnChanges، تبدیل عدد امتیاز یک محصول، به عرض div نمایش ستارهها است.
مکانیزم کار رخداد ngOnChanges و دریافت اطلاعات از والد
متد ngOnChanges، تنها به خواص ویژهای به نام «input properties» واکنش نشان میدهد. اگر یک کامپوننت تو در توی قرار گرفتهی در یک کامپوننت دیگر، بخواهد اطلاعاتی را از والد خود دریافت کند، باید خاصیتی را در معرض دید آن دربرگیرنده قرار دهد. این کار توسط decorator ویژهای به نام ()Input@ انجام میشود.
به همین جهت است که پیش از خاصیت rating در کلاس StarComponent، شاهد درج مزین کنندهی ویژهی ()Input@ هستیم:
export class StarComponent implements OnChanges { @Input() rating: number;
پس از آن، کامپوننت دربرگیرنده یا والد، این خاصیت ورودی ویژه را از طریق روش property binding متداول، مقدار دهی میکند:
[rating]='product.starRating'
بدیهی است در اینجا چون خاصیت starWidth از نوع ورودی تعریف نشدهاست، قابلیت property binging فوق را در کامپوننت والد، ندارد.
اکنون به ازای هر بار نمایش این کامپوننت فرزند، خاصیت rating ورودی آن مقدار دهی شده و مقدار آن در رخداد ngOnChanges قابل دسترسی و استفاده خواهد بود. اینجا است که میتوان از این مقدار تغییر یافته، جهت ترجمهی آن به عرض div نمایش ستارهها، استفاده کرد.
ارسال دادهها از کامپوننت فرزند به کامپوننت والد
تا اینجا با استفاده از «خواص ورودی» امکان دسترسی به مقادیر ارسالی از طرف والد را در کامپوننت فرزند، پیدا کردیم. عکس آن نیز امکان پذیر است؛ اما توسط رخدادها.
کامپوننت فرزند، با استفاده از decorator ویژهی دیگری به نام ()Output@ امکان ارسال رخدادها را به کامپوننت والد پیدا میکند:
export class StarComponent implements OnChanges { @Input() rating: number; starWidth: number; @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();
در مثال جاری اگر کاربر بر روی div ستارههای نمایش داده شده کلیک کند، اتصال به آن از طریق event binging متداول انجام میشود (متد جدید onClick به رخداد click متصل شدهاست):
<div class="crop" [style.width.px]="starWidth" [title]="rating" (click)='onClick()'>
onClick() { this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); }
تا اینجا مرحلهی تنظیمات رخدادها در کامپوننت فرزند صورت گرفت. ابتدا خاصیتی از نوع Output تعریف شد. سپس در کدهای قالب این کامپوننت جدید، متد onClick به رخداد click متصل گردید و سپس در کدهای مدیریت کنندهی این متد، متد ratingClicked.emit جهت ارسال اطلاعات نهایی به والد، فراخوانی گردید.
اکنون در کامپوننت والد، باید این مراحل برای دریافت اطلاعات از کامپوننت فرزند خود، طی شوند:
الف) ابتدا نام خاصیت مزین شدهی با Output، به عنوان مقصد event binding مشخص میشود و سپس متدی در کلاس کامپوننت والد، به آن متصل میگردد:
(ratingClicked)='onRatingClicked($event)'
ب) در ادامه، تعریف این متد جدید متصل شده را به کلاس ProductListComponent اضافه میکنیم:
onRatingClicked(message: string): void { this.pageTitle = 'Product List: ' + message; }
به این ترتیب با کلیک بر روی div هر کامپوننت نمایش ستارهای امتیازها، خاصیت pageTitle درج شدهی در صفحه تغییر میکند.
استفاده از کامپوننت نمایش ستارهای امتیازها
نکات کلی افزودن این کامپوننت جدید، تفاوتی با مطالب عنوان شدهی در قسمت سوم، در حین بررسی مراحل افزودن دایرکتیو نمایش لیست محصولات، به کامپوننت ریشهی سایت ندارد و یکی هستند.
برای افزودن و استفاده از این کامپوننت جدید، ابتدا قالب product-list.component.html را گشوده و سپس سطر نمایش عددی امتیاز یک محصول را به نحو ذیل تغییر میدهیم:
<td> <ai-star [rating]='product.starRating' (ratingClicked)='onRatingClicked($event)'> </ai-star> </td>
سپس باید به کلاس کامپوننت لیست محصولات (کامپوننت در برگیرنده) اعلام کرد که این کامپوننت جدید را باید از کجا پیدا کند. برای این منظور فایل product-list.component.ts را گشوده و خاصیت directives این کامپوننت را مقدار دهی میکنیم:
import { Component, OnInit } from 'angular2/core'; import { IProduct } from './product'; import { ProductFilterPipe } from './product-filter.pipe'; import { StarComponent } from '../shared/star.component'; @Component({ selector: 'pm-products', templateUrl: 'app/products/product-list.component.html', styleUrls: ['app/products/product-list.component.css'], pipes: [ProductFilterPipe], directives: [StarComponent] })
نمونهای از اجرای برنامه را در تصویر ذیل مشاهده میکنید:
در اینجا ستون امتیازهای محصولات با کامپوننت نمایش ستارهای این امتیازها جایگزین شدهاست و همچنین با کلیک بر روی یکی از آنها، عنوان panel جاری تغییر کردهاست.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part6.zip
خلاصهی بحث
در اینجا نحوهی طراحی API عمومی یک کامپوننت را بررسی کردیم. تا زمانیکه خواص کلاس یک کامپوننت به نحو متداولی تعریف میشوند، میدان دید آنها محدود است به قالب تعریف شدهی متناظر با آنها. اگر نیاز است خاصیتی خارج از این قالب و به صورت عمومی در کامپوننت دربرگیرندهی دیگری در دسترس قرار گیرد، آنرا با مزین کنندهی ()Input@ مشخص میکنیم و اگر قرار است این کامپوننت فرزند، اطلاعاتی را به کامپوننت والد ارسال کند، اینکار را توسط رخدادها و با تعریف ویژگی ()Output@ و EventEmitter انجام میدهد. نوع آرگومان جنریک EventEmitter، تعیین کنندهی نوع اطلاعاتی است که قرار است به کامپوننت دربرگیرنده ارسال شوند.
پس از تعریف کامپوننت فرزند، برای تعریف آن در کامپوننت والد، از نام selector آن به عنوان یک المان جدید HTML استفاده میشود و سپس با استفاده از property binding، اطلاعات لازم، به خاصیت از نوع ()Input@ کامپوننت فرزند ارسال میگردد. از event binding برای دریافت رخدادها از کامپوننت فرزند استفاده میشود. در اینجا هر رخدادی که توسط مزین کنندهی ()Output@ تعریف شده باشد، میتواند به عنوان مقصد event binding تعریف شود و اگر نیاز است به رخدادهای property binding از والد به فرزند، گوش فرا داد، میتوان اینترفیس OnChanges را در کلاس کامپوننت فرزند پیاده سازی کرد.
My last post investigated ways to build a .NET Core desktop/console app with a web-rendered UI without bringing in the full weight of Electron. This seems to have interested a lot of people, so I decided to upgrade it to newer technologies and add cross-platform support.
The result is a little NuGet package called WebWindow that you can add to any .NET Core console app. It can open a native OS window (Windows/Mac/Linux) containing web-based UI, without your app having to bundle either Node or Chromium.
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
using System.ComponentModel.DataAnnotations; namespace BlazorWasmUpload.Shared { public class User { [Required] public string Name { get; set; } [Required] [Range(18, 90)] public int Age { get; set; } } }
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { [HttpPost] public async Task<IActionResult> CreateUser( [FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null)
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Forms; namespace BlazorWasmUpload.Client.Services { public interface IFilesManagerService { Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName); } public class FilesManagerService : IFilesManagerService { private readonly HttpClient _httpClient; public FilesManagerService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>( string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName) { var requestContent = new MultipartFormDataContent(); requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); if (browserFiles?.Any() == true) { foreach (var file in browserFiles) { var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000); requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name); } } requestContent.Add( content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"), name: modelParameterName); var result = await _httpClient.PostAsync(requestUri, requestContent); result.EnsureSuccessStatusCode(); return result; } } }
- کامپوننت استاندارد InputFiles در Blazor Wasm، میتواند لیستی از IBrowserFileهای انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- model جنریک، برای نمونه وهلهای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- در ادامه یک MultipartFormDataContent را تشکیل دادهایم. توسط این ساختار میتوان فایلها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContentها را اضافه میکنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل میشوند. متد OpenReadStream به صورت پیشفرض فقط فایلهایی تا حجم 500 کیلوبایت را پردازش میکند و اگر فایلی حجیمتر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت میتوان توسط پارامتر maxAllowedSize آن، این مقدار پیشفرض را تغییر داد.
- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شدهاست. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشتهی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشتهی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی میتوان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال میشود؛ چون MultipartFormDataContent ویژهای را داریم، model-binder پیشفرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.
- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده میکنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.
- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.
پس از تهیهی سرویس ویژهی فوق که میتواند اطلاعات فایلها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگیها در فایل Program.cs برنامهی کلاینت است:
namespace BlazorWasmUpload.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IFilesManagerService, FilesManagerService>(); // ... } } }
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
@code { IReadOnlyList<IBrowserFile> SelectedFiles; User UserModel = new User(); bool isProcessing; string UploadErrorMessage;
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
@page "/" @using System.IO @using BlazorWasmUpload.Shared @using BlazorWasmUpload.Client.Services @inject IFilesManagerService FilesManagerService <h3>Post a model with files</h3> <EditForm Model="UserModel" OnValidSubmit="CreateUserAsync"> <DataAnnotationsValidator /> <div> <label>Name</label> <InputText @bind-Value="UserModel.Name"></InputText> <ValidationMessage For="()=>UserModel.Name"></ValidationMessage> </div> <div> <label>Age</label> <InputNumber @bind-Value="UserModel.Age"></InputNumber> <ValidationMessage For="()=>UserModel.Age"></ValidationMessage> </div> <div> <label>Photos</label> <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" /> @if (!string.IsNullOrWhiteSpace(UploadErrorMessage)) { <div> @UploadErrorMessage </div> } @if (SelectedFiles?.Count > 0) { <table> <thead> <tr> <th>Name</th> <th>Size (bytes)</th> <th>Last Modified</th> <th>Type</th> </tr> </thead> <tbody> @foreach (var selectedFile in SelectedFiles) { <tr> <td>@selectedFile.Name</td> <td>@selectedFile.Size</td> <td>@selectedFile.LastModified</td> <td>@selectedFile.ContentType</td> </tr> } </tbody> </table> } </div> <div> <button disabled="@isProcessing">Create user</button> </div> </EditForm>
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
private void OnInputFileChange(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 15); if (args.FileCount == 0 || files.Count == 0) { UploadErrorMessage = "Please select a file."; return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { UploadErrorMessage = "Please select .jpg/.jpeg/.png files only."; return; } SelectedFiles = files; UploadErrorMessage = string.Empty; }
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
private async Task CreateUserAsync() { try { isProcessing = true; await FilesManagerService.PostModelWithFilesAsync( requestUri: "api/Files/CreateUser", browserFiles: SelectedFiles, fileParameterName: "inputFiles", model: UserModel, modelParameterName: "userModel"); UserModel = new User(); } finally { isProcessing = false; SelectedFiles = null; } }
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
using System.IO; using Microsoft.AspNetCore.Mvc; using BlazorWasmUpload.Shared; using Microsoft.AspNetCore.Hosting; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; using System.Text.Json; using BlazorWasmUpload.Server.Utils; using System.Linq; namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { private const int MaxBufferSize = 0x10000; private readonly IWebHostEnvironment _webHostEnvironment; private readonly ILogger<FilesController> _logger; public FilesController( IWebHostEnvironment webHostEnvironment, ILogger<FilesController> logger) { _webHostEnvironment = webHostEnvironment; _logger = logger; } [HttpPost] public async Task<IActionResult> CreateUser( //[FromForm] string userModel, // <-- this is the actual form of the posted model [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null) { /*var user = JsonSerializer.Deserialize<User>(userModel); _logger.LogInformation($"userModel.Name: {user.Name}"); _logger.LogInformation($"userModel.Age: {user.Age}");*/ _logger.LogInformation($"userModel.Name: {userModel.Name}"); _logger.LogInformation($"userModel.Age: {userModel.Age}"); var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } if (inputFiles?.Any() == true) { foreach (var file in inputFiles) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true); await file.CopyToAsync(fileStream); _logger.LogInformation($"Saved file: {filePath}"); } } return Ok(); } } }
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
using System; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace BlazorWasmUpload.Server.Utils { public class JsonModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType); if (result != null) { bindingContext.Result = ModelBindingResult.Success(result); return Task.CompletedTask; } } return Task.CompletedTask; } } }
[HttpPost] public async Task<IActionResult> CreateUser( [FromForm] string userModel, // <-- this is the actual form of the posted model [FromForm] IList<IFormFile> inputFiles = null) { var user = JsonSerializer.Deserialize<User>(userModel);
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آنها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شدهی از فایلهای کانفیگ به کلاسها پیش بینی شدهاست.
- امکان بارگذاری مجدد فایلهای کانفیگ درصورت تغییر، بدون ریاستارت کل برنامه وجود دارد.
- امکان تزریق وابستگیهای تنظیمات برنامه، به قسمتهای مختلف آن پیش بینی شدهاست.
نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON
برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بستهی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:
البته همانطور که در تصویر مشاهده میکنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بستههای مرتبط با خواندن فایلهای کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Json": "1.0.0" },
افزودن یک فایل کانفیگ JSON دلخواه
بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
<appSettings> <add key="Logging-IncludeScopes" value="false" /> <add key="Logging-Level-Default" value="verbose" /> <add key="Logging-Level-System" value="Information" /> <add key="Logging-Level-Microsoft" value="Information" /> </appSettings>
در فایل JSON فوق، نمونهای از key/valueها، آرایهها و اطلاعات چندین سطحی را مشاهده میکنید.
خواندن فایل تنظیمات appsettings.json در برنامه
پس از نصب پیشنیاز خواندن فایلهای کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازندهی جدیدی را به آن اضافه کنید:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json"); Configuration = builder.Build(); }
الف) این خواندن، در سازندهی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آنرا به یک خاصیت عمومی انتساب دادهایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شدهاست. این اطلاعات را میتوان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه میکنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت میشود.
دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot
تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شدهی در این اینترفیس به چه صورتی است؟
همانطور که مشاهده میکنید، هر سطح از سطح قبلی آن با : جدا شدهاست. همچنین اعضای آرایه، دارای ایندکسهای 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات میتوان نوشت:
var key1 = Configuration["Key1"]; var user1 = Configuration["Auth:Users:0"]; var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray(); var loggingIncludeScopes = Configuration["Logging:IncludeScopes"]; var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن میتوان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
سرویس IConfigurationRoot قابل تزریق است
در قسمت قبل، سرویسها و تزریق وابستگیها را بررسی کردیم. نکتهی جالبی را که میتوان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup { public IConfigurationRoot Configuration { set; get; }
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده میکنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آنرا چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container(); container.Configure(config => { config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
ب) فایل project.json کتابخانهی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ "dependencies": { //same as before "Microsoft.Extensions.Configuration.Abstractions": "1.0.0" }
ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازندهی سرویس MessagesService تزریق میکنیم:
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; public MessagesService(IConfigurationRoot configurationRoot) { _configurationRoot = configurationRoot; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; return $"DNT {key1}"; } }
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده میکند:
public void Configure( IApplicationBuilder app, IHostingEnvironment env, IMessagesService messagesService) { app.Run(async context => { var siteName = messagesService.GetSiteName(); await context.Response.WriteAsync($"Hello {siteName}"); }); }
خواندن تنظیمات از حافظه
الزاما نیازی به استفاده از فایلهای JSON و یا XML در اینجا وجود ندارد. ابتداییترین حالت کار با بستهی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا میتوان لیستی از key/valueها را ذکر کرد:
var builder = new ConfigurationBuilder() .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), });
var theValue = Configuration["the-key"];
امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی
در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder میتوان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایلهای کانفیگ دیگر را اضافه کرد:
public class Startup { public IConfigurationRoot Configuration { set; get; } public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddInMemoryCollection(new[] { new KeyValuePair<string,string>("the-key", "the-value"), }) .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false) .AddJsonFile($"appsettings.{env}.json", optional: true); Configuration = builder.Build(); }
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی میکند.
امکان دسترسی به متغیرهای محیطی سیستم عامل
در انتهای زنجیرهی ConfigurationBuilder میتوان متد AddEnvironmentVariables را نیز ذکر کرد:
var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) .AddEnvironmentVariables();
امکان نگاشت تنظیمات برنامه به کلاسهای متناظر
کار کردن با key/valueهای رشتهای، هرچند روش پایهای استفادهی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شدهاست. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{ "Key1": "Value1", "Auth": { "Users": [ "Test1", "Test2", "Test3" ] }, "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } }, "Smtp": { "Server": "0.0.0.1", "User": "user@company.com", "Pass": "123456789", "Port": "25" } }
در این کتابخانهی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels { public class SmtpConfig { public string Server { get; set; } public string User { get; set; } public string Pass { get; set; } public int Port { get; set; } } }
سپس به پروژهی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آنرا بتوان در پروژهی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{ "dependencies": { // same as before "Core1RtmEmptyTest.ViewModels": "1.0.0-*" },
و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوهی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services) { services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژهی Core1RtmEmptyTest.ViewModels به لایهی سرویس برنامه اضافه میکنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینهی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بستهی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایهی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگیهای فایل project.json این لایه چنین شکلی را پیدا میکند:
"dependencies": { "Core1RtmEmptyTest.ViewModels": "1.0.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0", "Microsoft.Extensions.Options": "1.0.0", "NETStandard.Library": "1.6.0" }
public interface IMessagesService { string GetSiteName(); } public class MessagesService : IMessagesService { private readonly IConfigurationRoot _configurationRoot; private readonly IOptions<SmtpConfig> _settings; public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings) { _configurationRoot = configurationRoot; _settings = settings; } public string GetSiteName() { var key1 = _configurationRoot["Key1"]; var server = _settings.Value.Server; return $"DNT {key1} - {server}"; } }
اکنون اگر برنامه را جرا کنید، این خروجی را میتوان مشاهده کرد (که در آن آدرس Server دریافت شدهی از فایل کانفیگ نیز مشخص است):
البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگیها در تمام قسمتهای برنامه کار میکند. برای مثال در کنترلرها هم میتوان <IOptions<SmtpConfig را به همین نحو تزریق کرد.
نحوهی واکنش به تغییرات فایلهای کانفیگ
در نگارشهای قبلی ASP.NET، هر تغییری در فایل web.config، سبب ریاستارت شدن کل برنامه میشد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران میشد.
در ASP.NET Core، برنامهی وب ما دیگر متکی به فایل web.config نبوده و همچنین میتوان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایلهای کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
ERROR TypeError: "this.fileInput2 is undefined"
<ng-template #templateUploads> <div> <strong>{{titleModal}}</strong> <button type="button" aria-label="Close" (click)="closeModal()"> <span aria-hidden="true">×</span> </button> </div> <div> <form #upladForm="ngForm" (submit)="submitUpload()" novalidate> <fieldset> <div> <label>فایل</label> <div> <input required #fileInput2 type="file"> </div> </div> <div> <button (click)="upload()" type="button"> upload </button> <button type="submit" > close </button> </div> </fieldset> </form> </div> </ng-template>
@ViewChild('fileInput2') fileInput2: ElementRef; constructor(...... submitUpload(form: NgForm) { const fileInput2File: HTMLInputElement = this.fileInput2.nativeElement; .....