MVVM و نمایش دیالوگها
- در حالت کلی برای مدیریت Commands فقط کافی است قسمت canExecute آنها را مدیریت کنید (مثلا در delegate command معرفی شده در همین سری). اگر false برگرداند (مثلا بر اساس سطح دسترسی کاربر جاری)، خودبخود عنصر مرتبط با آن غیرفعال خواهد شد.
یک نکتهی تکمیلی: نحوهی نامگذاری ویژهی عناصر در فرمهای جدید Blazor SSR
اگر با نگارشهای دیگر Blazor کار کرده باشید، عموما یک EditForm را به صفحه اضافه کرده و چند المان را به آن اضافه میکنیم و ... کار میکند. حتی اگر کامپوننتهای سفارشی را هم بر این مبنا تهیه کنیم ... بازهم بدون نکتهی خاصی کار میکنند. اما ... در برنامههای Blazor SSR اینطور نیست! زمانیکه برای مثال مدل فرم را به این صورت تعریف میکنیم:
[SupplyParameterFromForm] public OrderPlace? MyModel { get; set; }
و آنرا به نحو متداولی در صفحه نمایش میدهیم:
<InputText @bind-Value="MyModel.City"/>
اگر به المان رندر شدهی در مرورگر مراجعه کنیم، ویژگی name حاصل، با MyModel.City مقدار دهی شدهاست و ... این موضوع درج نام خاصیت مدل (و یا اصطلاحا Html Field Prefix)، برای Blazor SSR بسیار مهم است! تاحدی که اگر از آن آگاه نباشید، ممکن است ساعتی را مشغول دیباگ برنامه شوید که چرا، مقدار نالی را دریافت کردهاید و یا عناصر تعریف شدهی در کامپوننتهای سفارشی، کار نمیکنند و مقدار نمیگیرند!
متاسفانه API بازگشت نام کامل عناصری که توسط Blazor SSR تولید میشود، عمومی نیست و internal است. اگر از کامپوننتهای استاندارد خود Blazor استفاده میکنید، نیازی نیست تا به این موضوع فکر کنید و مدیریت آن خودکار است؛ اما همینکه قصد تولید کامپوننتهای سفارشی مخصوص SSR را داشته باشید، اولین مشکلی را که با آن مواجه خواهید شد، دقیقا همین مسالهی تولید صحیح HtmlFieldPrefixها است.
برای رفع این مشکل و دسترسی به API پشت صحنهی تولید نام فیلدها در Blazor SSR، میتوان از کامپوننت پایهی InputBase خود Blazor ارثبری کرد و به این ترتیب به خاصیت جدید NameAttributeValue آن دسترسی یافت (این خاصیت به داتنت 8 و مخصوص Blazor SSR، اضافه شدهاست) که اینکار در کلاس BlazorHtmlField انجام شدهاست. روش استفادهی از آن هم به صورت زیر است:
private BlazorHtmlField<T?> ValueField => new(ValueExpression ?? throw new InvalidOperationException(message: "Please use @bind-Value here.")); [Parameter] public T? Value { set; get; } [Parameter] public EventCallback<T?> ValueChanged { get; set; } [Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;
زمانیکه میخواهیم در یک کامپوننت سفارشی، خاصیتی bind پذیر را طراحی کنیم، روش کار آن، مانند مثال فوق است که به همراه یک خاصیت، یک EventCallback و یک Expression است تا اعتبارسنجی و انقیاد دوطرفه را فعال کند. اما ... اگر همین Value را مستقیما در فیلدهای کامپوننت استفاده کنیم ... مقدار نمیگیرد؛ چون به همراه نام کامل خاصیت بایند شدهی به آن نیست. برای مثال بجای MyModel.City فقط City درج میشود (که به علت نداشتن .MyModel، سیستم binding از مقدار آن صرفنظر میکند). اکنون با استفاده از BlazorHtmlField فوق، میتوان به نام کامل تولیدی توسط Blazor SSR دسترسی یافت و از آن استفاده کرد:
<input type="text" dir="ltr" name="@ValueField.HtmlFieldName" id="@ValueField.HtmlFieldName" />
HtmlFieldName ای که در اینجا درج میشود، توسط خود Blazor محاسبه شده و با انتظارات موتور binding آن تطابق دارد و دیگر به خواص بایند شدهای که مقدار نمیگیرند، نخواهیم رسید.
طبقه بندی Bad Code Smell ها
sealed class DeviceStatus : IEquatable<DeviceStatus> { [Flags] private enum StatusRepresentation { AllFine = 0, NotOperational = 1, VisiblyDamaged = 2, CircuitryFailed = 4 } private StatusRepresentation Representation { get; } private DeviceStatus(StatusRepresentation representation) { this.Representation = representation; } public static DeviceStatus AllFine() => new DeviceStatus(StatusRepresentation.AllFine); public DeviceStatus WithVisibleDamage() => new DeviceStatus(this.Representation | StatusRepresentation.VisiblyDamaged); public DeviceStatus NotOperational() => new DeviceStatus(this.Representation | StatusRepresentation.NotOperational); public DeviceStatus Repaired() => new DeviceStatus(this.Representation & ~StatusRepresentation.NotOperational); public DeviceStatus CircuitryFailed() => new DeviceStatus(this.Representation | StatusRepresentation.CircuitryFailed); public DeviceStatus CircuitryReplaced() => new DeviceStatus(this.Representation & ~StatusRepresentation.CircuitryFailed); public override int GetHashCode() => (int)this.Representation; public override bool Equals(object obj) => this.Equals(obj as DeviceStatus); public bool Equals(DeviceStatus other) => other != null && this.Representation == other.Representation; public static bool operator ==(DeviceStatus a, DeviceStatus b) => (object.ReferenceEquals(a, null) && object.ReferenceEquals(b, null)) || (!object.ReferenceEquals(a, null) && a.Equals(b)); public static bool operator !=(DeviceStatus a, DeviceStatus b) => !(a == b); }
جلوگیری از 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 توسط خواص یا متدهای همزمان فراخوانی شده بعدی، قفل شدهاست.
چرا برنامه نویسی موبایل؟
با افزایش روزافزون SmartPhone ها و تبلتها، بازار تکنولوژی به این سمت سوق پیدا کردهاست. از این رو شرکتهای ارائه دهنده نرم افزاری، از این موقعیت استفاده کرده و هر کدام پلتفرم متفاوتی را برای برنامه نویسی بر روی این اسمارت فونها ارائه دادهاند. یکی از بزرگترین دغدغههای امروزه شرکتهای برنامه نویسی و توسعه نرم افزار موبایل، انتخاب درست پلتفرم برای توسعه نرم افزار میباشد. در این مقاله قصد دارم یکی از این پلت فرمها را بررسی کرده و معرفی کنم.
شرکت xamarin کار خود را در سال 2011 با ارایه نسخه Cross Platform پلتفرم .Net به نام Mono آغاز کرد. بعد از ارایه این نسخه از .Net، زامارین به کمک Mono توانست پیاده سازی بر روی Android و IOS را به نامهای MonoForAndroid و MonoTouch ارایه دهد. بعد از این نسخهها برنامه نویسان توانستند بر روی سیستم عاملهای اندروید و آی او اس به صورت Native کد خود را به زبان C# نوشته و آنها را اجرا کنند.
چرا باید از Xamarin استفاده کنم؟
در ادامه مقاله قصد دارم شما را با برخی از ویژگیهای زامارین آشنا کرده و مزایای استفاده از آن را بیان کنم.
Xamarin امکانی را فراهم کردهاست که برنامه نویسان به دو روش متفاوت قادر خواهند بود برنامههای خود را بنویسند:
Xamarin Native :1
زامارین به شما این امکان را میدهد که بتوانید به صورت مستقیم برای هر پلتفرم به صورت جداگانه برنامه نویسی کنید. در اندروید اینکار با Xamarin.Droid و در IOS اینکار با Xamarin.Touch امکانپذیر است. مزیتهای استفاده از این روش عبارتند از:
· بهره گیری از یک زبان برنامه نویسی
همانطور که میدانید یادگیری یک زبان برنامه نویسی هزینهی زیادی را برای یک سازمان و یا یک شخص به همراه دارد. در زامارین این امکان فراهم شدهاست که با استفاده از تنها یک زبان برنامه نویسی مانند C# ، برنامه نویسان بتوانند برای پلتفرمهای مختلف برنامه بنویسند. در نظر داشته باشید که UI در هر پلتفرم به صورت جداگانه پیاده سازی میشود. به طور مثال در اندروید به وسیلهی Android Xml میتوانید ظاهر برنامه خود را پیاده سازی کنید و منطقهای خود را با زبان C# برای تمامی پلت فرمها به صورت یکسان بنویسید.
· تجربه کاربری Native
زامارین به شما این امکان را خواهد داد که با استفاده از کنترلهای Native هر پلتفرم به تجربه کاربری همان پلت فرم دسترسی پیدا کنید و اپلیکیشنی با ظاهر و UX همان پلتفرم بسازید.
· استفاده 100% از امکانات هر پلتفرم
زامارین به دلیل Native بودن این امکان را به برنامه نویسان ارائه میدهد که با استفاده از یک زبان و با بکارگیری Cycle Life مخصوص هر پلتفرم، به 100% امکانات و API های هر پلتفرم دسترسی پیدا کنند.
· Performance
به دلیل اینکه برنامههای زامارین به صورت Native اجرا میشوند Performance بالایی دارند.
· دسترسی به API های موجود در .Net
شما قادر خواهید بود با دانش موجود مانند Entity Framework Code و.. به فریم ورک .Net دسترسی پیدا کرده و از API های درون آن استفاده کنید. زامارین از یکی از پیاده سازیهای .Net Standard استفاده میکند.
· استفاده مجدد از کد
در زامارین قادر خواهید بود که از کدهای خود، استفاده مجدد کرده و این امر سبب مدیریت بهتر بر روی کد، کد نویسی کمتر، هزینه نگهداری کد کمتر، توسعه راحتتر اپلیکیشن و ... میشود.
· تست خودکار
در زامارین شما میتوانید برای کدهای خود تست خودکار نوشته و آنها را به صورت خودکار تست کنید.
· Bind کردن Library های Objective-C و Java
زامارین طوری طراحی شدهاست که دست شما را در هیچگونه شرایطی نخواهد بست. شما میتوانید به صورت مستقیم کدهایی را که به زبان های Java و Objective-C نوشته شدهاند، به پروژه اضافه کرده و هیچگونه نگرانی از بابت کدهای از قبل نوشته شده که به زبانهای Objective-C و Java هستند، نداشته باشید.
· Designer
در زمارین این امکان وجود دارد که در هر پلتفرم از طریق Designer مخصوص به آن پلتفرم، UI خود را طراحی و پیاده سازی کنید.
· Async
در برنامه نویسی غیر همزمان ( Asynchronous Programming ) این امکان وجود دارد که برنامه شما بدون توقف، یک قسمت از کد را اجرا کرده و منتظر اجرای قسمتهای دیگر کد نشود؛ یا به اصطلاح برنامه از حالت Response خارج نشود. در زبانهای Java ، Objective-C و Swift اینکار باید با CallBack و به صورت Manual مدیریت شود؛ اما #C این امکان را فراهم آورده است که به راحتی اینکار را انجام داده و برنامه خود را همیشه در حالت پاسخ دهی نگه دارید.
public async Task<List<FeedItem>> GetFeedItems(DateTime date) { var feed = "http://planet.xamarin.com/feed/"; var response = await httpClient.GetStringAsync(feed); var items = await ParseFeedAsync(response); return items.Where(item => item.Published.Date == date).ToList(); }
· Parallel Programming
در برنامه نویسی موازی( Parallel Programming ) برخلاف برنامه نویسی MultiThread که بر روی یک هسته CPU اجرا میشود، بر روی چندین هسته CPU به صورت موازی اجرا میشود. زامارین از این نوع برنامه نویسی پشتیبانی میکند.
Xamarin.Forms: 2
پس از معرفی Xamarin Forms API شما میتوانید علاوه بر مزیتهایی که در بالا اشاره شد، کدهای Logic خود را با زبان C# و کدهای UI خود را با زبان XAML پیاده سازی کرده و با یک بار نوشتن کد، در پلتفرمهای مختلف خروجی خود را مشاهده کنید. مزیت استفاده از Xamarin Forms عبارتند از:
· استفاده از کد واحد برای پیاده سازی UI و Logic
یکی از بهترین مزیتهایی را که میتوان به آن اشاره نمود این است که شما کافیست یک بار کد خود را بنویسید و Xamarin Forms کد شما را در پلت فرمهای متفاوت پیاده سازی خواهد کرد.
<?xml version="1.0" encoding="UTF-8"?> <TabbedPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MyApp.MainPage"> <TabbedPage.Children> <ContentPage Title="Profile" Icon="Profile.png"> <StackLayout Spacing="20" Padding="20" VerticalOptions="Center"> <Entry Placeholder="Username" Text="{Binding Username}"/> <Entry Placeholder="Password" Text="{Binding Password}" IsPassword="true"/> <Button Text="Login" TextColor="White" BackgroundColor="#77D065" Command="{Binding LoginCommand}"/> </StackLayout> </ContentPage> <ContentPage Title="Settings" Icon="Settings.png"> <!-- Settings --> </ContentPage> </TabbedPage.Children> </TabbedPage>
برای پیاده سازی UI در Xamarin Forms باید از XAML استفاده کنید. همچنین مانند روش قبلی میتوانید از زبان C# برای پیاده سازی منطق، استفاده نمایید.
با استفاده از Xamarin Forms شما تجربه نوشتن کد Cross Platform را در کنار Native بودن آن خواهید داشت.
· استفاده از یک کنترل مخصوص یک پلتفرم در بین کد ( Embedding )
در Xamarin Forms این امکان وجود دارد که اگر شما خواستید یک کنترل مخصوص IOS را در بین کدهای خود استفاده کنید، بتوانید به راحتی اینکار را انجام دهید.( (Embedding
اگر به تصویر بالا دقت کنید متوجه خواهید شد که در یکسری از کنترلها، تصاویر متفاوت هستند. در نسخه اندروید یک Action Button در قسمت پایین صفحه مشاهده میکنید که در نسخهی IOS آن موجود نیست. یعنی به صورت مستقیم کنترل Action Button که مختص به پلت فرم اندروید میباشد، درون Xamarin Forms استفاده شده است.
· دسترسی به هر پلتفرم به طور مستقیم
شما قادر خواهید بود به طور مستقیم به هر پلت فرم دسترسی پیدا کرده و به طور مثال در هر پلتفرم، UI مخصوص به خود را با Property های مخصوص به خود طراحی کنید.
· UITest و Test Cloud
در Xamarin Forms میتوانید برای UI خود تست نوشته و آنها را به وسیله Xamarin Test Cloud بر روی صدها Device متفاوت تست کنید. (این امکان فقط برای Android و IOS وجود دارد.)
· Life Cycle مشابه در تمامی پلتفرم ها
همانطور که میدانید پلتفرمهای مختلف، Life Cycle های متفاوتی برای مدیریت اپلیکیشن دارند. یکی از مزیتهای استفاده از Xamarin Forms این است که شما میتوانید برای تمامی پلتفرمها بهوسیلهی یک Life Cycle یکسان کد بنویسید.
· Previewer
یکی از بهترین قابلیتهایی که در Xamarin Forms اضافه شدهاست این است که شما قادر خواهید بود به صورت Real Time خروجی فایل XAML خود را به وسیله Previewer مشاهده نمایید.
· Performance Profiler
به وسیله Xamarin Profiler شما میتوانید میزان مصرف حافظه، Performance و ... را در اندروید و IOS اندازه گیری نمایید.
نکات قابل توجه:
Ø استفاده همزمان از Xamarin Forms و Xamarin Native
شما میتوانید کدهای خود را با حداکثر Reusability نوشته و در صورت لزوم با کدهای Xamarin Native ترکیب کنید.
Ø Documentation خیلی خوب
زمارین مستندات جامع و کاملی را در سایت خود گردآوری کرده که میتوانید به راحتی از آن برای فهم تمامی قسمتهای Xamarin استفاده کنید.
معرفی کتابخانهی OxyPlot
نمایش tracker آن با حرکت ماوس، بجای کلیک بر روی نقاط (حالت پیش فرض)
private IPlotController _controller; public IPlotController Controller { get { if (_controller == null) { // show tracker with mouse move _controller = new PlotController(); _controller.BindMouseEnter(PlotCommands.HoverPointsOnlyTrack); } return _controller; } }
Controller="{Binding Controller}"
در قسمت قبل، در حین بررسی رفتار جزیرههای تعاملی Blazor Server، نکتهی زیر را هم دربارهی راهبری صفحات SSR مرور کردیم:
« اگر دقت کنید، جابجایی بین صفحات، با استفاده از fetch انجام شده؛ یعنی با اینکه این صفحات در اصل static HTML خالص هستند، اما ... کار full reload صفحه مانند ASP.NET Web forms قدیمی انجام نمیشود (و یا حتی برنامههای MVC و Razor pages) و نمایش صفحات، Ajax ای است و با fetch استاندارد آن صورت میگیرد تا هنوز هم حس و حال SPA بودن برنامه حفظ شود. همچنین اطلاعات DOM کل صفحه را هم بهروز رسانی نمیکند؛ فقط موارد تغییر یافته در اینجا به روز رسانی خواهند شد.»
در این قسمت، نکات تکمیلی این قابلیت را که به آن enhanced navigation هم گفته میشود، بررسی میکنیم.
روش غیرفعال کردن راهبری بهبودیافته برای بعضی از لینکها
ویژگی راهبری بهبودیافته فقط در حین هدایت بین صفحات مختلف یک برنامهی Blazor 8x SSR، فعال است. اگر در این بین، کاربری به یک صفحهی غیر بلیزری هدایت شود، راهبری بهبود یافته شکست خورده و سعی میکند حالت full document load را پیاده سازی و اجرا کند. مشکل اینجاست که در این حالت دو درخواست ارسال میشود: ابتدا حالت راهبری بهبودیافته فعال میشود و در ادامه پس از شکست این راهبری، هدایت مستقیم صورت میگیرد. برای رفع این مشکل میتوان ویژگی جدید data-enhance-nav را با مقدار false، به لینکهای خارجی مدنظر اضافه کرد تا برای این حالتها دیگر ویژگی راهبری بهبودیافته فعال نشود:
<a href="/not-blazor" data-enhance-nav="false">A non-Blazor page</a>
فعالسازی مدیریت بهبودیافتهی فرمهای SSR
در قسمت چهارم این سری با فرمهای جدید SSR مخصوص Blazor 8x آشنا شدیم. این فرمها هم میتوانند از امکانات راهبری بهبود یافته استفاده کنند (یعنی مدیریت ارسال آن، توسط fetch API انجام شده و به روز رسانی قسمتهای تغییریافتهی صفحه را Ajax ای انجام دهند)؛ برای نمونه اینبار همانند تصویر زیر، از fetch استاندارد برای ارسال اطلاعات به سمت سرور کمک گرفته میشود (یعنی عملیات Ajax ای شده؛ بجای یک post-back معمولی):
اما ... این قابلیت به صورت پیشفرض در فرمهای تعاملی SSR غیرفعال است. چون همانطور که عنوان شد، اگر مقصد این فرم، یک آدرس غیربلیزری باشد، دوبار ارسال فرم صورت خواهد گرفت؛ یکبار با استفاده از fetch API و بار دیگر پس از شکست، به صورت معمولی. اما اگر مطمئن هستید که endpoint این فرم، قطعا یک کامپوننت بلیزری است، بهتر است این قابلیت را در یک چنین فرمهایی نیز به صورت زیر فعال کنید:
<form method="post" @onsubmit="() => submitted = true" @formname="name" data-enhance> <AntiforgeryToken /> <InputText @bind-Value="Name" /> <button>Submit</button> </form> @if (submitted) { <p>Hello @Name!</p> } @code { bool submitted; [SupplyParameterFromForm] public string Name { get; set; } = ""; }
<EditForm method="post" Model="NewCustomer" OnValidSubmit="() => submitted = true" FormName="customer" Enhance> <DataAnnotationsValidator /> <ValidationSummary/> <p> <label> Name: <InputText @bind-Value="NewCustomer.Name" /> </label> </p> <button>Submit</button> </EditForm> @if (submitted) { <p id="pass">Hello @NewCustomer.Name!</p> } @code { bool submitted = false; [SupplyParameterFromForm] public Customer? NewCustomer { get; set; } protected override void OnInitialized() { NewCustomer ??= new(); } public class Customer { [StringLength(3, ErrorMessage = "Name is too long")] public string? Name { get; set; } } }
نکتهی مهم: در این حالت فرض بر این است که هیچگونه هدایتی به یک Non-Blazor endpoint صورت نمیگیرید؛ وگرنه با یک خطا مواجه خواهید شد.
غیرفعال کردن راهبری بهبودیافته برای قسمتی از صفحه
اگر با استفاده از جاواسکریپت و خارج از کدهای بیلزر، اطلاعات DOM را بهروز رسانی میکنید، ویژگی راهبری بهبودیافته، از آن آگاهی نداشته و به صورت خودکار تمام تغییرات شما را بازنویسی میکند. به همین جهت اگر نیاز است قسمتی از صفحه را که مستقیما توسط کدهای جاواسکریپتی تغییر میدهید، از بهروز رسانیهای این قابلیت مصون نگهدارید، میتوانید ویژگی جدید data-permanent را به آن قسمت اضافه کنید:
<div data-permanent> Leave me alone! I've been modified dynamically. </div>
امکان آگاه شدن از بروز راهبری بهبودیافته در کدهای جاواسکریپتی
اگر به هردلیلی در کدهای جاواسکریپتی خودنیاز به آگاه شدن از وقوع یک هدایت بهبودیافته را دارید (برای مثال جهت بازنویسی تغییرات ایجاد شدهی توسط آن)، میتوانید به نحو زیر، مشترک رخدادهای آن شوید:
<script> Blazor.addEventListener('enhancedload', () => { console.log('enhanced load event occurred'); }); </script>
ویژگی جدید Named Element Routing در Blazor 8x
Blazor 8x از ویژگی مسیریابی سمت کلاینت به کمک تعریف URL fragments پشتیبانی میکند. به این صورت رسیدن (اسکرول) به یک قسمت از صفحهای طولانی، بسیار ساده میشود.
برای مثال المان h2 با id مساوی targetElement را درنظر بگیرید:
<div class="border border-info rounded bg-info" style="height:500px"></div> <h2 id="targetElement">Target H2 heading</h2> <p>Content!</p>
<a href="/counter#targetElement"> <NavLink href="/counter#targetElement"> Navigation.NavigateTo("/counter#targetElement");
معرفی متد جدید Refresh در Blazor 8x
در Blazor 8x، امکان بارگذاری مجدد صفحه با فراخوانی متد جدید NavigationManager.Refresh(bool forceLoad = false) میسر شدهاست. این متد در حالت پیشفرض از قابلیت راهبری بهبودیافته برای به روز رسانی صفحه استفاده میکند؛ مگر اینکه اینکار میسر نباشد. اگر آنرا با پارامتر true فراخوانی کنید، full-page reload رخ خواهد داد.
همین اتفاق در مورد متد Navigation.NavigateTo نیز رخدادهاست. این متد نیز در Blazor 8x به صورت پیشفرض بر اساس قابلیت راهبری بهبود یافته کار میکند؛ مگر اینکه اینکار میسر نباشد و یا پارامتر forceLoad آنرا به true مقدار دهی کنید.
ایجاد کامپوننت جدید BlazorJS
برای بررسی نحوهی تعامل جاوا اسکریپت و Blazor، در ابتدا کامپوننت جدید Pages\LearnBlazor\BlazorJS.razor را ایجاد کرده:
@page "/BlazorJS" <h3>BlazorJS</h3> @code { }
<li class="nav-item px-3"> <NavLink class="nav-link" href="BlazorJS"> <span class="oi oi-list-rich" aria-hidden="true"></span> BlazorJS </NavLink> </li>
روش فراخوانی کدهای جاوا اسکریپتی از طریق کدهای سیشارپ Blazor
فرض کنید میخواهیم در حین کلیک بر روی دکمهای مانند دکمهی حذف، ابتدا تائیدیهای را توسط تابع confirm جاوا اسکریپتی، از کاربر اخذ کنیم. روش انجام چنین کاری در برنامههای مبتنی بر Blazor به صورت زیر است:
@page "/BlazorJS" @inject IJSRuntime JsRuntime <h3>BlazorJS</h3> <div class="row"> <button class="btn btn-secondary" @onclick="TestConfirmBox">Test Confirm Button</button> </div> <div class="row"> @if (ConfirmResult) { <p>Confirmation has been made!</p> } else { <p>Confirmation Pending!</p> } </div> @code { string ConfirmMessage = "Are you sure you want to click?"; bool ConfirmResult; async Task TestConfirmBox() { ConfirmResult = await JsRuntime.InvokeAsync<bool>("confirm", ConfirmMessage); } }
- در اینجا میخواهیم تابع استاندارد confirm جاوا اسکریپتی را از طریق کدهای سیشارپ، با کلیک بر روی دکمهی Test Confirm Button، فراخوانی کنیم. به همین جهت onclick@ این دکمه، به متد TestConfirmBox کدهای UI سیشارپ این کامپوننت، متصل شدهاست.
- برای دسترسی به توابع جاوا اسکریپتی، نیاز است سرویس توکار IJSRuntime را به کدهای کامپوننت تزریق کنیم که روش انجام آنرا توسط دایرکتیو inject@ مشاهده میکنید. برای دسترسی به این سرویس توکار، نیاز به تنظیمات ابتدایی خاصی نیست و اینکار پیشتر انجام شدهاست.
- سرویس JsRuntime تزریق شده، دو متد مهم InvokeVoidAsync و InvokeAsync را جهت فراخوانی توابع جاوا اسکریپتی به همراه دارد. اگر تابعی، خروجی غیر void داشته باشد، باید از متد InvokeAsync استفاده کرد. برای مثال خروجی تابع استاندارد confirm، از نوع boolean است. بنابراین نوع این خروجی را به صورت یک آرگومان جنریک متد InvokeAsync مشخص کردهایم.
- اولین پارامتر متد InvokeAsync، نام رشتهای تابع جاوا اسکریپتی است که قرار است صدا زده شود. پارامترهای اختیاری بعدی که به صورت params object?[]? args تعریف شدهاند، لیست نامحدود آرگومانهای ورودی این متد هستند.
- فیلد ConfirmMessage، پیامی را جهت اخذ تائید، تعریف میکند که به عنوان پارامتر متد confirm، توسط JsRuntime.InvokeAsync فراخوانی خواهد شد.
- فیلد ConfirmResult، نتیجهی فراخوانی متد confirm جاوا اسکریپتی را به همراه دارد.
- در اینجا روش عکس العمل نشان دادن به خروجی دریافتی از متد جاوااسکریپتی را نیز مشاهده میکنید. پس از پایان متد TestConfirmBox که یک متد رویدادگران است، همانطور که در مطلب بررسی «چرخهی حیات کامپوننتها» نیز بررسی کردیم، متد StateHasChanged، در پشت صحنه فراخوانی میشود که سبب رندر مجدد UI خواهد شد. بنابراین در حین رندر مجدد UI، بر اساس مقدار جدید ConfirmResult دریافت شدهی از کاربر، با تشکیل یک if/else@، میتوان به نتیجهی تائید یا عدم تائید کاربر، واکنش نشان داد. با این توضیحات در اولین بار نمایش کامپوننت جاری چون مقدار ConfirmResult مساوی false است، پیام زیر را مشاهده میکنیم:
اما در ادامه با کلیک بر روی دکمه و تائید پیام ظاهر شده، عبارت زیر ظاهر میشود:
روش افزودن یک کتابخانهی خارجی جاوا اسکریپتی به پروژههای Blazor
فرض کنید میخواهیم پیامهای برنامه را توسط کتابخانهی معروف جاوا اسکریپتی Toastr نمایش دهیم؛ با این دمو.
مرحلهی اول کار با این کتابخانه، دریافت فایلهای CSS و JS آن است. برای این منظور قصد داریم از برنامهی مدیریت بستههای LibMan استفاده کنیم:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli libman init libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap libman install jquery --provider unpkg --destination wwwroot/lib/jquery libman install toastr --provider unpkg --destination wwwroot/lib/toastr
البته تعاریف مداخل آنها به فایل libman.json نیز اضافه میشوند. مزیت آن، اجرای دستور libman restore برای بازیابی و نصب مجدد تمام بستههای ذکر شدهی در فایل libman.json است.
پس از دریافت بستههای سمت کلاینت آن، مداخل مرتبط را به فایل Pages\_Host.cshtml برنامهی Blazor Server اضافه خواهیم کرد (و یا در فایل wwwroot/index.html برنامههای Blazor WASM).
<head> <base href="~/" /> <link rel="stylesheet" href="lib/toastr/build/toastr.min.css" /> </head> <body> <script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/toastr/build/toastr.min.js"></script> <script src="_framework/blazor.server.js"></script> </body>
یک نکته: میتوان فایل csproj برنامه را به صورت زیر تغییر داد تا کار اجرای دستور libman restore را قبل از build، به صورت خودکار انجام دهد:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <Target Name="DebugEnsureLibManEnv" BeforeTargets="BeforeBuild" Condition=" '$(Configuration)' == 'Debug' "> <!-- Ensure libman is installed --> <Exec Command="libman --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="libman is required to build and run this project. To continue, please run `dotnet tool install -g Microsoft.Web.LibraryManager.Cli`, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'libman'. This may take several minutes..." /> <Exec WorkingDirectory="$(MSBuildProjectDirectory)" Command="libman restore" /> </Target> </Project>
روش فراخوانی یک کتابخانهی خارجی جاوا اسکریپتی در پروژههای Blazor
پس از افزودن فایلهای سمت کلاینت toastr و تعریف مداخل آن در فایل Pages\_Host.cshtml برنامهی Blazor Server جاری، اکنون میخواهیم از این کتابخانه استفاده کنیم. یک روش کار با این نوع کتابخانههای عمومی و سراسری به صورت زیر است:
- ابتدا فایل خالی جدید wwwroot\js\common.js را ایجاد میکنیم.
- سپس تابع عمومی و سراسری ShowToastr را بر اساس امکانات کتابخانهی toastr و مستندات آن، به صورت زیر ایجاد میکنیم:
window.ShowToastr = (type, message) => { // Toastr don't work with Bootstrap 4.2 toastr.options.toastClass = "toastr"; // https://github.com/CodeSeven/toastr/issues/599 if (type === "success") { toastr.success(message, "Operation Successful", { timeOut: 20000 }); } if (type === "error") { toastr.error(message, "Operation Failed", { timeOut: 20000 }); } };
سطر اول آن هم برای رفع عدم تداخل با بوت استرپ 4x اضافه شدهاست. بوت استرپ 4x به همراه کلاسهای CSS مشابهی است که نیاز است با تنظیم toastClass به مقداری دیگر، این تداخل را برطرف کرد.
- در ادامه مدخل تعریف فایل wwwroot\js\common.js را به انتهای تگ body فایل Pages\_Host.cshtml اضافه میکنیم:
<script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/toastr/build/toastr.min.js"></script> <script src="js/common.js"></script> <script src="_framework/blazor.server.js"></script> </body>
در آخر برای آزمایش آن به کامپوننت Pages\LearnBlazor\BlazorJS.razor مراجعه کرده و تابع سراسری ShowToastr را دقیقا مانند روشی که در مورد تابع confirm بکار بردیم، توسط سرویس JsRuntime، فراخوانی میکنیم:
@page "/BlazorJS" @inject IJSRuntime JsRuntime <div class="row"> <button class="btn btn-success" @onclick="@(()=>TestSuccess("Success Message"))">Test Toastr Success</button> <button class="btn btn-danger" @onclick="@(()=>TestFailure("Error Message"))">Test Toastr Failure</button> </div> @code { async Task TestSuccess(string message) { await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message); } async Task TestFailure(string message) { await JsRuntime.InvokeVoidAsync("ShowToastr", "error", message); } }
کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی
میتوان جهت کاهش تکرار کدهای استفاده از تابع ShowToastr، متدهای الحاقی زیر را برای سرویس IJSRuntime تهیه کرد:
using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorServerSample.Utils { public static class JSRuntimeExtensions { public static ValueTask ToastrSuccess(this IJSRuntime JSRuntime, string message) { return JSRuntime.InvokeVoidAsync("ShowToastr", "success", message); } public static ValueTask ToastrError(this IJSRuntime JSRuntime, string message) { return JSRuntime.InvokeVoidAsync("ShowToastr", "error", message); } } }
@using BlazorServerSample.Utils
async Task TestSuccess(string message) { //await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message); await JsRuntime.ToastrSuccess(message); }
فراخوانی یک متد عمومی واقع در کامپوننت فرزند از طریق کامپوننت والد
فرض کنید در کامپوننت فرزند Pages\LearnBlazor\LearnBlazorComponents\ChildComponent.razor که در قسمتهای قبل آنرا تکمیل کردیم، متد عمومی زیر تعریف شدهاست:
@inject IJSRuntime JsRuntime @code { // ... public async Task TestSuccess(string message) { await JsRuntime.ToastrSuccess(message); } }
@page "/ParentComponent" <ChildComponent OnClickBtnMethod="ShowMessage" @ref="ChildComp" Title="This title is passed as a parameter from the Parent Component"> <ChildContent> A `Render Fragment` from the parent! </ChildContent> <DangerChildContent> A danger content from the parent! </DangerChildContent> </ChildComponent> <div class="row"> <button class="btn btn-success" @onclick="@(()=>ChildComp.TestSuccess("Done!"))">Show Alert</button> </div> @code { ChildComponent ChildComp; // ... }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-11.zip
<Button Text="This is a test button" />
Tabbed Page نیز چندین Tab را نمایش میدهد که هر Tab خود یک Content Page است. Carousel Page نیز همانند Tabbed Page است، ولی با Swipe کردن به چپ و راست میشود بین صفحات چرخید. هر دوی اینها Multi Page محسوب میشوند. MasterDetail نیز این امکان را میدهد که از بغل منویی برای Swipe کردن وجود داشته باشد. در نهایت Navigation Page محتوای یک Content Page را نمایش میدهد، ولی در بالای آن Navigation Bar دارد؛ شامل دکمه بازگشت به صفحه قبل و Title صفحه جاری و ...
علاوه بر Page ها، Layoutها نیز وجود دارند. برای مثال، Stack Layout برای چینش خطی (افقی یا عمودی) استفاده میشود. Grid برای ساختار شبکهای استفاده میشود و Flex Layout عملکردی مشابه با Flex در وب دارد.
برای مثال، در صورتی که بخواهید چهار دکمه را هم اندازه با هم نمایش دهید، دارید:
<Grid> <Button Text="1" Grid.Row="0" Grid.Column="0" /> <Button Text="2" Grid.Row="0" Grid.Column="1" /> <Button Text="3" Grid.Row="1" Grid.Column="0" /> <Button Text="4" Grid.Row="1" Grid.Column="1" /> </Grid>
در نهایت کنترلها را داریم. برای مثال Label، Button و ... هر کدام از اینها نقشی را ایفا میکنند و امکاناتی دارند.
پس Page داریم، داخل Page از Layout استفاده میکنیم برای چینش کلی صفحه و در نهایت از کنترلهای Image، ListView، Button و ... استفاده میکنیم تا ظاهر فرم تکمیل شود.
هر Page علاوه بر ظاهر خود، دارای یک منطق نیز هست. منطق، کاری است که آن فرم انجام میدهد. برای مثال فرم لاگین میتواند یک Stack Layout عمودی باشد، شامل یک Entry برای گرفتن نام کاربری، یک Entry برای گرفتن رمز عبور، که IsPassword آن True است و در نهایت یک دکمه که برای انجام عمل لاگین است.
در قسمت منطق که با CSharp نوشته میشود، ما یک Property از جنس string برای نگه داشتن نام کاربری داریم. یک Property از جنس string برای نگه داشتن رمز عبور و یک Command که عمل لاگین را انجام دهد. Property اول با نام UserName به Text آن Entry اول وصل میشود (به اصطلاح Bind میشود) و همین طور Property دوم با نام Password نیز به Text آن Entry دوم که IsPassword اش True بود وصل میشود و در نهایت Command لاگین به دکمه لاگین وصل میشود.
برای زدن ظاهر فرم لاگین، در پروژه XamApp روی فولدر Views راست کلیک نموده و از منوی Add به New Item رفته و Content Page را میزنیم. نام آن را LoginView.xaml میگذاریم که داخل تگ Content Page خواهیم داشت:
<StackLayout Orientation="Vertical"> <Entry Placeholder="User name" Text="{Binding UserName}" /> <Entry IsPassword="True" Placeholder="Password" Text="{Binding Password}" /> <Button Command="{Binding LoginCommand}" Text="Login" /> </StackLayout>
برای زدن منطق، در پروژه XamApp روی فولدر ViewModels راست کلیک نموده و از منوی Add گزینه Class را انتخاب کرده و نام آن را LoginViewModel.cs میگذاریم که در داخل آن خواهیم داشت:
public class LoginViewModel : BitViewModelBase { public string UserName { get; set; } public string Password { get; set; } public BitDelegateCommand LoginCommand { get; set; } public LoginViewModel() { LoginCommand = new BitDelegateCommand(Login); } public async Task Login() { // Login implementation ... } }
BitDelegateCommand در این مثال، وظیفه اجرای متد Login را به عهده دارد و آن را اجرا میکند؛ زمانیکه کاربر روی دکمه لاگین Click یا Tap کند.
برای این که Content Page جدید، یعنی LoginView به همراه منطق آن، یعنی LoginViewModel در برنامه نشان داده شوند، لازم است با Navigation به آن صفحه برویم. برای این کار، ابتدا باید این زوج را رجیستر کنیم. برای این کار به متد RegisterTypes در کلاس App رفته (زیر فایل App.xaml یک فایل App.xaml.cs است) و خط زیر را به آن اضافه میکنیم:
containerRegistry.RegisterForNav<LoginView, LoginViewModel>("Login");
حال در متد OnInitializedAsync در چند خط بالاتر داریم:
await NavigationService.NavigateAsync("/Login", animated: false);
این سطر باعث میشود که Navigation Service که همان طور که از اسمش بر میآید، کارش Navigation بین صفحات است، صفحه لاگین را باز کند.
هم اکنون پروژه XamApp بروز شده و دارای این مثال است. در صورتی که آن را الآن Clone کنید و یا در صورتی که از قبل گرفته بودید، دستور git pull را برای گرفتن آخرین تغییرات بزنید، میتوانید این کدها رو داخل پروژه داشته باشید.
برنامه را اجرا کنید و در متد Login، یک Break point بگذارید. سپس برنامه را اجرا کنید. User Name و Password را پر کنید و بر روی دکمه لاگین بزنید. خواهید دید که متد لاگین اجرا میشود و User Name و Password با مقادیری که نوشته بودید، پر شدهاند.
هنوز موارد زیادی برای آموزش باقی مانده، اما با این توضیحات میتوانید در محیط توسعهای که آماده کردهاید، فرمهایی ساده را پیاده سازی کنید و برایشان منطقهایی ساده را بنویسید و به برنامه بگویید که در ابتدای اجرا آن، صفحه را برای شما باز کند. در قسمت بعدی، به صورت عمیقتر وارد UI میشویم.