ممکن است برای بعضی پروژهها پیش امده باشد که بخواهید پارامترهای مختلف را از طریق خط فرمان از کاربر دریافت کنید. command line parser این امکان را به شما میدهد تا کلاس هایی را تعریف کرده و دستورات و پارامترهای خط فرمانی مورد نظرتان را به آنها map کنید. به این وسیله شما در برنامه به جای پردازش پارامترهای ورودی، با نمونههای (instance) کلاسهای مورد نظر طرف هستید.
dotnet add package linq2db.EntityFrameworkCore
LinqToDB.EntityFrameworkCore.LinqToDBForEFTools.Initialize(); LinqToDB.Data.DataConnection.TurnTraceSwitchOn();
public class MemberHierarchyCTE { public int ChildId { set; get; } public int? ParentId { set; get; } }
var memberHierarchyCte = context.CreateLinqToDbContext().GetCte<MemberHierarchyCTE>(memberHierarchy => { return ( from member in context.Members select new MemberHierarchyCTE { ChildId = member.MemId, ParentId = member.RecommendedBy } ) .Concat ( from member in context.Members from hierarchy in memberHierarchy .InnerJoin(hierarchy => member.MemId == hierarchy.ParentId) select new MemberHierarchyCTE { ChildId = hierarchy.ChildId, ParentId = member.RecommendedBy } ); });
WITH [memberHierarchy] ([ChildId], [ParentId]) AS ( SELECT [member_1].[MemId], [member_1].[RecommendedBy] FROM [Members] [member_1] UNION ALL SELECT [hierarchy_1].[ChildId], [member_2].[RecommendedBy] FROM [Members] [member_2] INNER JOIN [memberHierarchy] [hierarchy_1] ON [member_2].[MemId] = [hierarchy_1].[ParentId] )
در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر میشوم تا مدیریت کدهای شما را در اندروید راحتتر کند.
یک نکتهی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده میشوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارتهای ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آنها اشاره میکنیم، نه آنچه که کاربران Eclipse به آن اشاره میکنند.
یک. برای هر تکه کد و یا متدی که مینویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
/** * * <p> * check network is available or not <br/> * internet connection is not matter,for check internet connection refer to IsInternetConnected() Method in this class * </p> * <p> * Required Permission : <b>android.permission.ACCESS_NETWORK_STATE</b> * </p> * @param context * @return returns true if a network is available */ public boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); }
public class ProjectSettings { public static NotificationsId=new NotificationsId(); public static UrlAddresss=new UrlAddresss(); public static SdPath=new SdPath(); ...... }
ProjectSettings.NotificationsId.UpdateNotificationId
سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام میدهند:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
public interface IMessageUI { void ShowToast(Context context,String message); } public class MessageUI impelement IMessageUI { public void ShowToast(Context context,string message) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show(); } }
چهار. اگر برای اولین بار است وارد اندروید میشوید، خوب چرخههای یک شیء، چون اکتیویتی یا فراگمنت را یاد بگیرید تا در آینده با مشکلات خاصی روبرو نشوید.
چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده میشود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد دادهها نباشد.
تا بدینجا اکتیویتی مشکلی ندارد و میتواند به عملیات پاسخ دهد ولی اگر قسمتی از اکتیویتی در زیر لایهای از UI پنهان شود، به عنوان مثال دیالوگی باز شود که قسمتی از اکتیویتی را بپوشاند و یا منویی همانند تلگرام قسمتی از صفحه را بپوشاند، اکتیویتی اصطلاحا در حالت Pause قرار گرفته و بدین ترتیب رویداد OnPause اجرا میگردد. اگر همین دیالوگ بسته شود و مجددا اکتیویتی به طور کامل نمایان گردد مجددا رویداد OnResume اجرا میگردد.
از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفتهاید رویداد OnStop اجرا شدهاست و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز میگردند.
شش. اگر برنامه شما قرار است در چندین حالت مختلفی که اتفاق میافتد، یک کار خاصی را انجام دهد، برای برنامهتان یک Receiver بنویسید و در آن کدهای تکراری را نوشته و در محلهای مختلف وقوع آن رویدادها، رسیور را صدا بزنید. برای نمونه برنامه تلگرام یک سرویس پیام رسان پشت صحنه دارد که در دو رویداد قرار است اجرا شوند. یکی موقعی که گوشی بوت خود را تکمیل کرده است و در حال آغاز فرایندهای سیستم عامل است و دیگر زمانی است که برنامه اجرا میشود. در اینجا تلگرام از یک رسیور سیستمی برای آگاهی از بوت شدن و یک رسیور داخل برنامه جهت آگاهی از اجرای برنامه استفاده میکند و هر دو به یک کلاس از جنس BroadcastReceiver متصلند:
<receiver android:name=".AppStartReceiver" android:enabled="true"> <intent-filter> <action android:name="org.telegram.start" /> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>
public class AppStartReceiver extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { AndroidUtilities.runOnUIThread(new Runnable() { @Override public void run() { ApplicationLoader.startPushService(); } }); } }
برنامه تلگرام حتی برای حالتهای پخش هم رسیورها استفاده کرده است که در همین رسیور وضعیت تغییر پلیر مشخص میشود:
<receiver android:name=".MusicPlayerReceiver" > <intent-filter> <action android:name="org.telegram.android.musicplayer.close" /> <action android:name="org.telegram.android.musicplayer.pause" /> <action android:name="org.telegram.android.musicplayer.next" /> <action android:name="org.telegram.android.musicplayer.play" /> <action android:name="org.telegram.android.musicplayer.previous" /> <action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.media.AUDIO_BECOMING_NOISY" /> </intent-filter> </receiver>
هفت. اگر از یک ORM برای لایه دادهها استفاده میکنید (قبلا در سایت جاری در مورد ORMهای اندروید صحبت کردهایم و ORMهای خوش دستی که خودم از آنها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه میکنند) در این نوع ORMها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on("poems.id=bank_poems_groups.poem") .where("BankGroup=?", String.valueOf(groupId)); return query.execute();
@Table(name="poems") public class Poem extends Model { public static String tableName="poems"; public static String codeColumn="code"; public static String titleColumn="title"; public static String bookColumn="book"; ...... @Column(name="code",index = true) public int Code; @Column(name="title") public String Title; @Column(name="book") public Book Book; .....}
From query= new Select() .from(Poem.class) .innerJoin(BankPoemsGroups.class) .on(Poem.TableName+"."+ Poem.IdColumn+"="+ BankPoemsGroups.TableName+"."+ BankPoemsGroups.PoemColumn) .where(Poem.BankGroupColumn+"=?", String.valueOf(groupId)); return query.execute();
public class QueryConcater { public String GetInnerJoinQuery(String table1,String field1,String table2,String field2) { String query=table1 +"." +field1+"="+table2+"."+field2; return query; } ...... }
return new Select() .from(Color.class) .innerJoin(ProductItem.class) .on(queryConcater.GetInnerJoinQuery(ProductItem.TableName, ProductItem.ColorColumn, Color.TableName)) .where(queryConcater.WhereConditionQuery (ProductItem.TableName, ProductItem.ProductColumn), productId) .execute();
هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که میتوانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم میکند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارشها شامل:
- وضعیت گوشی در حین باز شدن برنامه و در حین خطا چگونه بوده است.
- مشخصات گوشی
- این خطا به چه تعداد رخ داده است و برای چه تعداد کاربر
- گزارش گیری بر اساس اولین تاریخ رخداد خطا و آخرین تاریخ، نسخه سیستم عامل اندروید، ورژن برنامه شما و...
نه. آکرا همانند Elmah نمیتواند خطاهای catch شده را دریافت کند. برای حل این مشکل عبارت زیر را در catchها بنویسید:
ACRA.getErrorReporter().handleException(caughtException);
نمونه اشتباه:
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) { try { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); } catch (IOException e) { e.printStackTrace(); } }
public void CopyFile(String source,String destination,CopyFileListener copyFileListener) throws IOException { InputStream in = new FileInputStream(source); OutputStream out = new FileOutputStream(destination); long fileLength=new File(source).length(); // Transfer bytes from in to out byte[] buf = new byte[64*1024]; int len; long total=0; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); total+=len; copyFileListener.PublishProgress(fileLength,total); } in.close(); out.close(); }
public enum CustomerType { Person = 0, Company = 1 } public class Customer { public CustomerType Type { get; set; } }
با وجود چنین کلاسی از مشتری و نیاز به انجام فعالیتهای مختلفی بر روی آن، احتمالا نیاز خواهد بود که در بخشهای مختلف کد، گذارهی switch ای مانند زیر را اضافه کنید:
switch (customer.Type) { case CustomerType.Person: // calculate discount, or send message or edit customer or anything else break; case CustomerType.Company: // calculate discount, or send message or edit customer or anything else break; default: throw new ArgumentOutOfRangeException(); }
برای انجام فعالیتهای مختلفی مانند محاسبه تخفیف، ارسال پیام و یا ویرایش مشتری، نیاز خواهد بود این گذاره تکرار شود که خود این موضوع بوی بد duplicate code است و به الگوی shotgun surgery نیز ختم خواهد شد.
حال فرض کنید نیاز است مشتریان حقوقی، خود به دو نوع مشتری حقوقی بخش خصوصی و مشتری حقوقی بخش دولتی تقسیم شوند. در پیاده سازی ذکر شده باید به CustomerType یک آیتم افزوده شود و در تمامی switchها نیز در صورت نیاز شرط مربوط به آن اضافه شود.
برای حل این نوع از کد بد بو، معمولا یک کلاس پدر را به نام مشتری ایجاد کرده و کلاسهای مختص هر یک از انواع مشتری را از آن به ارث میبرند (Replace type code with subclass):
یا میتوان طراحی را کمی متفاوتتر و به صورت زیر انجام داد:
دلیل مشابه دیگر ایجاد این الگوی بد کد استفاده از type code به عنوان وضعیت یک تایپ است. که در این صورت میتوان بجای type code از state object استفاده کرد (Replace type code with strategy). به این مورد در مباحث مربوط به refactoring به طور مفصل پرداخته شده است.
جمع بندی
این کد بد بو در شرایط متفاوتی ایجاد میشود. با این حال یکی از پر تکرارترین آنها استفاده بد یا عدم استفاده از الگوهای طراحی شیء گرا است. تصحیص این الگوی بد، به خوانایی و نگهداری کد در بلند مدت کمک بسیار زیادی میکند.
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت 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
چرا از آنگولار به ری اکت + ری داکس سوئیچ کردم!
- مزیت کار کردن با TypeScript در مقایسه با ES6 خالص در React، امکان دسترسی به کامپایل آفلاین هست و مباحث پیشرفتهی کامپایلر مانند tree-shaking (حذف کدهای مرده) و AOT (a head of time compilation) که سبب میشن هم حجم نهایی کمتری تولید شود و هم پیش از اجرای برنامه در مرورگر و سپس یافتن باگهای احتمالی در زمان اجرا، پیش از موعد و توسط کامپایلر این باگها گزارش شوند. اگر قصد داشته باشید به یک چنین کیفیت و بررسی کدی در React برسید، باید تعداد آزمونهای واحد قابل توجهی را داشته باشین تا بتونید یافتن مشکلاتی را که کامپایلر TypeScript گوشزد میکند، شبیه سازی کنید. همچنین شما در TypeScript میتونید به تمام امکانات پیشرفتهی زبان جاوا اسکریپت (حتی پس از ES6) دسترسی داشته باشید، اما کد نهایی جاوا اسکریپتی تولید شدهی توسط آنرا برای ES5 که تمام مرورگرها از آن پشتیبانی میکنند، تولید کنید که این هم خودش یک مزیت مهم هست. بنابراین TypeScript فقط یک static type checker ساده نیست.
- اینکه Angular یک فریمورک هست به خودی خودش یک مزیت مهم هست نسبت به React که یک کتابخانه است و اجزای آن باید از منابع مختلفی تهیه شوند. فریم ورک یعنی به روز رسانیهای منظم تمام اجزای آن توسط خود تیم Angular و سازگاری کامل و یکدست هر جزء با نگارش فعلی یا همان آخرین نگارش موجود. اگر با دنیای وابستگیهای ثالث در یک پروژهی واقعی کار کرده باشید به خوبی میدونید که هر چقدر تعداد آنها کمتر باشند، نگهداری طولانی مدت آن پروژه آسانتر میشود؛ چون روزی ممکن است آن کتابخانهی ثالث دیگر توسعه پیدا نکند، یا منسوخ شود یا دیرتر از آخرین نگارش ارائه شده به روز رسانی شود. مزیت داشتن یک فریم ورک یکدست، درگیر نشدن با این مسایل است؛ خصوصا اینکه عموما کتابخانههای ثالث کیفیتشون در حد کتابخانهی اصلی نیست و اینکه مثلا خود تیم Angular ماژول روتر، اعتبارسنجی یا فرمهای اون رو توسعه میده، قطعا کیفیتشون از کتابخانههای ثالث دیگه بهتر هست.
- در مورد سرعت و کارآیی و حتی مصرف حافظه، مطابق یک benchmarck خیلی معتبر، وضعیت Angular اندکی بهتر از React است؛ هرچند در کل از این لحاظ به هم نزدیک هستند.
- این مباحث انحصاری شدن و اینها هم در مورد محصولات سورس باز، زیاد مفهومی ندارند و بیشتر یکسری شعار ایدئولوژیک هست توسط کسانیکه حتی تغییر رفتار این شرکتها را هم دنبال نمیکنند و منابع و ماخذی رو که مطالعه کردن مربوط به یک دهه قبل هست.
بر اساس آموزش مدیریت حالت در Blazor، قصد داریم یک سرویس پیام هشدار ساده، ولی زیبا را بوسیله کامپوننت Alert بوت استرپ ۵ ، بدون استفاده از توابع جاوا اسکریپتی، طراحی کنیم.
در ابتدا کتابخانههای css زیر را بوسیله LibMan به پروژه اضافه کرده و مداخل فایلهای را css نیز اضافه میکنیم:
{ "version": "1.0", "defaultProvider": "cdnjs", "libraries": [ { "provider": "unpkg", "library": "bootstrap@5.0.0", "destination": "wwwroot/lib/bootstrap" }, { "provider": "unpkg", "library": "open-iconic@1.1.1", "destination": "wwwroot/lib/open-iconic" }, { "provider": "unpkg", "library": "animate.css@4.1.1", "destination": "wwwroot/lib/animate" }, { "provider": "unpkg", "library": "bootstrap-icons@1.5.0", "destination": "wwwroot/lib/bootstrap-icons/" } ] }
public enum AlertType { Success, Info, Danger, Warning } public class AlertService { public void ShowAlert(string message, AlertType alertType, string animate = "animate__fadeIn") { OnChange?.Invoke(message, alertType,animate); } public event Action<string,AlertType, string> OnChange; }
services.AddScoped<AlertService>();
توضیحات:
در کدهای نهایی برنامه قرار است به این نحو کار نمایش Alertها را در کامپوننتهای مختلف انجام دهیم:
@inject AlertService AlertService @code { private void Success() { AlertService.ShowAlert("Success!", AlertType.Success); }
کدهای کامپوننت Alert.razor
@inject AlertService AlertService @implements IDisposable <style> .alert-show { display: flex; flex-direction: row; } .alert-hide { display: none; } </style> <div style="z-index: 5"> <div " + "alert-show" :"alert-hide")"> <i width="24" height="24"></i> <div> @Message </div> <button type="button" data-bs-dismiss="alert" aria-label="Close" @onclick="CloseClick"></button> </div> </div> @code { AlertType AlertType { get; set; } string Icon { get; set; } string Css { get; set; } string Animation { get; set; } private bool IsVisible { get; set; } private string Message { get; set; } System.Timers.Timer _alertTimeOutTimer; protected override void OnInitialized() { AlertService.OnChange += ShowAlert; } private void ShowAlert(string message, AlertType alertType, string animate) { _alertTimeOutTimer = new System.Timers.Timer { Interval = 5000, Enabled = true, AutoReset = false }; _alertTimeOutTimer.Elapsed += HideAlert; Message = message; switch (alertType) { case AlertType.Success: Css = "bg-success"; Icon = "bi-check-circle"; break; case AlertType.Info: Css = "bg-info"; Icon = "bi-info-circle-fill"; break; case AlertType.Danger: Css = "bg-danger"; Icon = "bi-exclamation-circle"; break; case AlertType.Warning: Css = "bg-warning"; Icon = "bi-exclamation-triangle-fill"; break; default: Css = Css; break; } AlertType = alertType; Animation = animate; IsVisible = true; InvokeAsync(StateHasChanged); } private void HideAlert(Object source, System.Timers.ElapsedEventArgs e) { IsVisible = false; InvokeAsync(StateHasChanged); _alertTimeOutTimer.Close(); } public void Dispose() { if (AlertService != null) AlertService.OnChange -= ShowAlert; if (_alertTimeOutTimer != null) { _alertTimeOutTimer.Elapsed -= HideAlert; _alertTimeOutTimer?.Dispose(); } } private void CloseClick() { IsVisible = false; _alertTimeOutTimer.Close(); InvokeAsync(StateHasChanged); } }
<div> <Alert></Alert>
نکتهای در استفاده از AutoMapper
من یکم گیج شدم. احساس میکنم عملیات اتومپر باید درون لایه سرویس انجام بشه که داخل اکشن فقط متدهای سرویس رو صدا بزنیم. از طرفی هم توی لایه سرویس که اومدم استفاده کنم که ویو-مدلم رو به کلاس ef نگاشت کنم و بعد بریزم توی دیتابیس دیدم که نمیشه ، چون لایه سرویس application_start برای کانفیگ اتومپر نداره ؟! لطفا راهنمایی نمایید. باتشکر
ابتدا لازم است Entity framework را توسط Nuget packages manager دانلود نمایید. سپس به پروژهی خود یک فولدر جدید را به نام Models و درون آن ابتدا یک کلاس را به نام User اضافه کرده و بدین صورت خواهیم داشت :
using System; namespace Console1.Models { public enum UserType { Admin, Ordinary } public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public UserType Type { get; set; } } }
UserType نیز کاملا مشخص است؛ هر User نقش Admin یا Ordinary را میتواند داشته باشد.
نوبت به نوشتن اینترفیس IUser میرسد. در همین پوشهای که قرار داریم، آن را پیاده سازی مینماییم:
namespace Console1.Models { public interface IUser { int UserId { get; set; } User User { get; set; } } }
هر entity که با User ارتباط دارد، باید اینترفیس فوق را پیاده سازی نماید. حال یک کلاس دیگر را به نام Post در همین پوشه درست کرده و بدین صورت پیاده سازی مینماییم.
using System.ComponentModel.DataAnnotations.Schema; namespace Console1.Models { public class Post : IUser { public int Id { get; set; } public string Context { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public User User { get; set; } } }
واضح است که relation از نوع one to many برقرار است و هر User میتواند n تا Post داشته باشد.
خوب تا اینجا کافیست و میخواهیم مدلهای خود را با استفاده از EF به Context معرفی کنیم. میتوانیم در همین پوشه کلاسی را به نام Context ساخته و بصورت زیر بنویسیم
using System.Data.Entity; namespace Console1.Models { public class Context : DbContext { public Context() : base("Context") { } public DbSet<User> Users { get; set; } public DbSet<Post> Posts { get; set; } } }
در اینجا مشخص کردهایم که دو Dbset از نوع User و Post را داریم. بدین معنا که EF دو table را برای ما تولید خواهد کرد. همچنین نام کلید رشتهی اتصالی به دیتابیس خود را نیز، Context معرفی کردهایم.
خوب تا اینجا قسمت اول پروژهی خود را تکمیل کردهایم. الان میتوانیم با استفاده از Migration دیتابیس خود را ساخته و همچنین رکوردهایی را بدان اضافه کنیم. در Package Manager Console خود دستور زیر را وارد نمایید:
enable-migrations
به صورت خودکار پوشهای به نام Migrations ساخته شده و درون آن Configuration.cs قرار میگیرد که آن را بدین صورت تغییر میدهیم:
namespace Console1.Migrations { using Models; using System.Data.Entity.Migrations; internal sealed class Configuration : DbMigrationsConfiguration<Console1.Models.Context> { public Configuration() { AutomaticMigrationsEnabled = true; } protected override void Seed(Console1.Models.Context context) { context.Users.AddOrUpdate(x => x.Id, new User { Id = 1, Name = "aaa", Age = 30, Type = UserType.Admin }, new User { Id = 2, Name = "bbb", Age = 20, Type = UserType.Ordinary }, new User { Id = 3, Name = "ccc", Age = 25, Type = UserType.Ordinary } ); context.Posts.AddOrUpdate(x => x.Id, new Post { Context = "ccc 1", UserId = 3 }, new Post { Context = "bbb 1", UserId = 2 }, new Post { Context = "bbb 2", UserId = 2 }, new Post { Context = "aaa 1", UserId = 1 }, new Post { Context = "bbb 3", UserId = 2 }, new Post { Context = "ccc 2", UserId = 3 }, new Post { Context = "ccc 3", UserId = 3 } ); context.SaveChanges(); } } }
در متد seed، رکوردهای اولیه را به شکل فوق وارد کرده ایم (رکوردها فقط به منظور تست میباشند*). در کنسول دستور Update-database را ارسال کرده، دیتابیس تولید خواهد شد.
قطعا مراحل بالا کاملا بدیهی بوده و نوشتن آنها بدین دلیل بوده که در Repository که الان میخواهیم شروع به نوشتنش کنیم به مدلهای فوق نیاز داریم تا بصورت کاملا عملی با مراحل کار آشنا شویم.
حال میخواهیم به پیاده سازی بخش اصلی این مقاله یعنی repository که از Row Level Security پشتیبانی میکند بپردازیم. در ریشهی پروژهی خود پوشهای را به نام Repository ساخته و درون آن کلاسی را به نام GenericRepository میسازیم. پروژهی شما هم اکنون باید ساختاری شبیه به این را داشته باشد.
GenericRepository.cs را اینگونه پیاده سازی مینماییم
using Console1.Models; using System; using System.Linq; using System.Linq.Dynamic; using System.Linq.Expressions; namespace Console1.Repository { public interface IGenericRepository<T> { IQueryable<T> CustomizeGet(Expression<Func<T, bool>> predicate); void Add(T entity); IQueryable<T> GetAll(); } public class GenericRepository<TEntity, DbContext> : IGenericRepository<TEntity> where TEntity : class, new() where DbContext : Models.Context, new() { private DbContext _entities = new DbContext(); public IQueryable<TEntity> CustomizeGet(Expression<Func<TEntity, bool>> predicate) { IQueryable<TEntity> query = _entities.Set<TEntity>().Where(predicate); return query; } public void Add(TEntity entity) { int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { ((IUser)entity).UserId = userId; } _entities.Set<TEntity>().Add(entity); } public IQueryable<TEntity> GetAll() { IQueryable<TEntity> result = _entities.Set<TEntity>(); int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { User me = _entities.Users.Single(c => c.Id == userId); if (me.Type == UserType.Admin) { return result; } else if (me.Type == UserType.Ordinary) { string query = $"{nameof(IUser.UserId).ToString()}={userId}"; return result.Where(query); } } return result; } public void Commit() { _entities.SaveChanges(); } } }
1) یک اینترفیس Generic را به نام IGenericRepository داریم که کلاس GenericRepository قرار است آن را پیاده سازی نماید.
2) این اینترفیس شامل متدهای CustomizeGet است که فقط یک predicate را گرفته و خیلی مربوط به این مقاله نیست (صرفا جهت اطلاع). اما متد Add و GetAll بصورت مستقیم قرار است هدف row level security را برای ما انجام دهند.
3) کلاس GenericRepository دو Type عمومی را به نام TEntity و DbContext گرفته و اینترفیس IGenericRepository را پیاده سازی مینماید. همچنین صریحا اعلام کردهایم TEntity از نوع کلاس و DbContext از نوع Context ایی است که قبلا نوشتهایم.
4) پیاده سازی متد CustomizeGet را مشاهده مینمایید که کوئری مربوطه را ساخته و بر میگرداند.
5) پیاده سازی متد Add بدین صورت است که به عنوان پارامتر، TEntity را گرفته (مدلی که قرار است save شود). بعد مشاهد میکنید که من به صورت hard code به UserId مقدار دادهام. قطعا میدانید که برای این کار به فرض اینکه از Asp.net Identity استفاده میکنید، میتوانید Claim آن Id کاربر Authenticate شده را بازگردانید.
با استفاده از IsAssignableFrom مشخص کردهایم که آیا TEntity یک Typeی از IUser را داشته است یا خیر؟ در صورت true بودن شرط، UserId را به TEntity اضافه کرده و بطور مثال در Serviceهای خود نیازی به اضافه کردن متوالی این فیلد نخواهید داشت و در مرحلهی بعد نیز آن را به entity_ اضافه مینماییم.
مشاهده مینمایید که این متد به قدری انعطاف پذیری دارد که حتی مدلهای مختلف به صورت کاملا یکپارچه میتوانند از آن استفاده نمایند.
6) به جالبترین متد که GetAll میباشد میرسیم. ابتدا کوئری را از آن Entity ساخته و در مرحلهی بعد مشخص مینماییم که آیا TEntity یک Typeی از IUser میباشد یا خیر؟ در صورت برقرار بودن شرط، User مورد نظر را یافته در صورتیکه Typeی از نوع Admin داشت، همهی مجموعه را برخواهیم گرداند (Admin میتواند همهی پستها را مشاهده نماید) و در صورتیکه از نوع Ordinary باشد، با استفاده از dynamic linq، کوئری مورد نظر را ساخته و شرط را ایجاد میکنیم که UserId برابر userId مورد نظر باشد. در این صورت بطور مثال همهی پستهایی که فقط مربوط به user خودش میباشد، برگشت داده میشود.
نکته: برای دانلود dynamic linq کافیست از طریق nuget آن را جست و جو نمایید: System.Linq.Dynamic
و اگر هم از نوع IUser نبود، result را بر میگردانیم. بطور مثال فرض کنید مدلی داریم که قرار نیست security روی آن اعمال شود. پس کوئری ساخته شده قابلیت برگرداندن همهی رکوردها را دارا میباشد.
7) متد Commit هم که پرواضح است عملیات save را اعمال میکند.
قبلا در قسمت Seed رکوردهایی را ساخته بودیم. حال میخواهیم کل این فرآیند را اجرا نماییم. Program.cs را از ریشهی پروژهی خود باز کرده و اینگونه تغییر میدهیم:
using System; using Console1.Models; using Console1.Repository; using System.Collections.Generic; using System.Linq; namespace Console1 { public class Program { public static int UserId = 1; //fake userId static void Main() { GenericRepository<Post, Context> repo = new GenericRepository<Post, Context>(); List<Post> posts = repo.GetAll().ToList(); foreach (Post item in posts) Console.WriteLine(item.Context); Console.ReadKey(); } } }
همانطور که ملاحظه میکنید، UserId به صورت fake ساخته شده است. آن چیزی که هم اکنون در دیتابیس رفته، بدین صورت است که UserId = 1 برابر Admin و بقیه Ordinary میباشند. در متد Main برنامه، یک instance از GenericRepository را گرفته و بعد با استفاده از متد GetAll و لیست کردن آن، همهی رکوردهای مورد نظر را برگردانده و سپس چاپ مینماییم. در صورتی که UserId برابر 1 باشد، توقع داریم که همهی رکوردها بازگردانده شود:
حال کافیست مقدار userId را بطور مثال تغییر داده و برابر 2 بگذاریم. برنامه را اجرا کرده و مشاهد میکنیم که با تغییر یافتن userId، عملیات مورد نظر متفاوت میگردد و به صورت زیر خواهد شد:
میبینید که تنها با تغییر userId رفتار عوض شده و فقط Postهای مربوط به آن User خاص برگشت داده میشود.
از همین روش میتوان برای طراحی Repositoryهای بسیار پیچیدهتر نیز استفاده کرد و مقدار زیادی از validationها را به طور مستقیم بدان واگذار نمود!
دانلود کدها در Github