ASP.NET MVC #9
مروری بر HTML Helpers استاندارد مهیا در ASP.NET MVC
یکی از اهداف وجودی Server controls در ASP.NET Web forms، رندر خودکار HTML است. برای مثال Menu control، TreeView control، GridView و امثال آن کار تولید تگهای table، tr و بسیاری موارد دیگر را در پشت صحنه برای ما انجام میدهند. اما در ASP.NET MVC، هدف رسیدن به یک markup ساده و تمیز است که 100 درصد بر روی اجزای آن کنترل داشته باشیم و این مورد به صورت ضمنی به این معنا است که در اینجا تمام این HTMLها را باید خودمان تولید کنیم. البته در عمل خیر. یک نمونه از آنرا در قسمت قبل مشاهده کردیم که چطور میتوان منطق تولید تگهای HTML را کپسوله سازی کرد و بارها مورد استفاده قرار داد. به علاوه فریم ورک ASP.NET MVC نیز به همراه تعدادی HTML helper توکار ارائه شده است مانند CheckBox، ActionLink، RenderPartial و غیره که کار تولید تگهای HTML ضروری و پایه را برای ما ساده میکنند.
یک مثال:
@Html.ActionLink("About us", "Index", "About")
در اینجا از متدی به نام ActionLink استفاده شده است. شیء Html هم وهلهای از کلاس HtmlHelper است که در تمام Viewها قابل دسترسی میباشد.
در این متد، اولین پارامتر، متن نمایش داده شده به کاربر را مشخص میکند، پارامتر سوم، نام کنترلری است که مورد استفاده قرار میگیرد و پارامتر دوم، نام متد یا اکشنی در آن است که فراخوانی خواهد شد (البته هر کدام از این HtmlHelperها به همراه تعداد قابل توجهی overload هم هستند).
زمانیکه این صفحه را رندر کنیم، به خروجی زیر خواهیم رسید:
<a href="/About">About us</a>
در این لینک نهایی خبری از متد Index ایی که معرفی کردیم، نیست. چرا؟
متد ActionLink بر اساس تعاریف پیش فرض مسیریابی برنامه، سعی میکند بهترین خروجی را ارائه دهد. مطابق تعاریف پیش فرض برنامه، متد Index، اکشن پیش فرض کنترلرهای برنامه است. بنابراین ضرورتی به ذکر آن ندیده است.
مثالی دیگر:
همان کلاسهای Product و Products قسمت هفتم را در نظر بگیرید (قسمت بررسی «ساختار پروژه مثال جاری» در آن مثال). همچنین به اطلاعات «نوشتن HTML Helpers ویژه، به کمک امکانات Razor» قسمت هشتم هم نیاز داریم.
اینبار میخواهیم بجای نمایش لیست سادهای از محصولات، ابتدا نام آنها را به صورت لینکهایی در صفحه نمایش دهیم. در ادامه پس از کلیک کاربر روی یک نام، توضیحات بیشتری از محصول انتخابی را در صفحهای دیگر ارائه نمائیم. کدهای View ما اینبار به شکل زیر تغییر میکنند:
@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>
@GetProductsList(@Model)
توضیحات:
ابتدا یک helper method را تعریف کردهایم و به کمک Html.ActionLink، از نام و شماره محصول، جهت تولید لینکهای نمایش جزئیات هر یک از محصولات کمک گرفتهایم. بنابراین در کنترلر خود نیاز به متد جدیدی به نام Details خواهیم داشت که پارامتری از نوع ProductNumber را دریافت میکند. سپس جزئیات این محصول را یافته و در View متناظر با خودش ارائه خواهد داد. پارامتر سومی که در متد ActionLink بکارگرفته شده در اینجا مشاهده میکنید، یک anonymously typed object است و توسط آن خواصی را تعریف خواهیم کرد که توسط تعاریف مسیریابی تعریف شده در فایل Global.asax.cs، قابل تفسیر و تبدیل به لینکهای مرتبط و صحیحی باشد.
اکنون اگر این مثال را اجرا کنیم، اولین لینک تولیدی آن به این شکل خواهد بود:
http://localhost/Home/Details/D123
در اینجا به یک نکته مهم هم باید دقت داشت؛ نام کنترلر به صورت خودکار به این لینک اضافه شده است. بنابراین بهتر است از ایجاد دستی این نوع لینکها خودداری کرده و کار را به متدهای استاندارد فریم ورک واگذار نمود تا بهترین خروجی را دریافت کنیم.
البته اگر الان بر روی این لینک کلیک نمائیم، با پیغام 404 مواجه خواهیم شد. برای تکمیل این مثال، متد Details را به کنترلر تعریف شده اضافه خواهیم کرد:
using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;
namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}
public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}
در متد Details، ابتدا ProductNumber دریافت شده و سپس شیء محصول متناظر با آن، به View این متد، بازگشت داده میشود. اگر بر اساس ورودی دریافتی، محصولی یافت نشد، کاربر را به View ایی به نام Error که در پوشه Views/Shared قرار گرفته است، هدایت میکنیم.
برای اضافه کردن این View هم بر روی متد کلیک راست کرده و گزینه Add view را انتخاب کنید. چون یک شیء strongly typed از نوع Product را قرار است به View ارسال کنیم (مانند مثال قسمت پنجم)، میتوان در صفحه باز شده تیک Create a strongly typed view را گذاشت و سپس Model class را از نوع Product انتخاب کرد و در قسمت Scaffold template هم Details را انتخاب نمود. به این ترتیب Code generator توکار VS.NET قسمتی از کار تولید View را برای ما انجام داده و بدیهی است اکنون سفارشی سازی این View تولیدی که قسمت عمدهای از آن تولید شده است، کار سادهای میباشد:
@model MvcApplication5.Models.Product
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Product</legend>
<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>
<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>
<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>
در اینجا کدهای مرتبط با View نمایش جزئیات محصول را مشاهده میکنید که توسط VS.NET به صورت خودکار از روی مدل انتخابی تولید شده است.
اکنون یکبار دیگر برنامه را اجرا کرده و بر روی لینک نمایش جزئیات محصولات کلیک نمائید تا بتوان این اطلاعات را در صفحهی بعدی مشاهده نمود.
یک نکته:
اگر سعی کنیم متد @helper GetProductsList فوق را در پوشه App_Code، همانند قسمت قبل قرار دهیم، به متد Html.ActionLink دسترسی نخواهیم داشت. چرا؟
پیغام خطایی که ارائه میشود این است:
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink'
به این معنا که در وهلهای از شیء System.Web.WebPages.Html.HtmlHelper، به دنبال متد ActionLink میگردد. در حالیکه ActionLink مورد نظر به کلاس System.Web.Mvc.HtmlHelper مرتبط میشود.
یک راه حل آن به صورت زیر است. به هر متد helper یک آرگومان WebViewPage page را اضافه میکنیم (به همراه دو فضای نامی که به ابتدای فایل اضافه میشوند)
@using System.Web.Mvc
@using System.Web.Mvc.Html
@using MvcApplication5.Models
@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
@MyHelpers.GetProductsList(this, @Model)
متد ActionLink و عبارات فارسی
متد ActionLink آدرسهای وبی را که تولید میکند، URL encoded هستند. برای نمونه اگر رشتهای که قرار است به عنوان پارامتر به اکشن متد ما ارسال شود، مساوی Hello World است، آنرا به صورت Hello%20World در صفحه درج میکند. البته این مورد مشکلی را در سمت متدهای کنترلرها ایجاد نمیکند، چون کار URL decoding خودکار است. اما ... اگر مقداری که قرار است ارسال شود مثلا «مقدار یک» باشد، آدرس تولیدی این شکل را خواهد داشت:
http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83
و اگر این URL encoding انجام نشود، فقط اولین قسمت قبل از فاصله به متد ارسال میگردد.
مرورگرهایی مثل فایرفاکس و کروم، مشکلی با نمایش این لینک به شکل اصلی فارسی آن ندارند (حین نمایش، URL decoding را اعمال میکنند). اما اگر مرورگر مثلا IE8 باشد، کاربر دقیقا به همین شکل آدرسها را در نوار آدرس مرورگر خود مشاهده خواهد کرد که آنچنان زیبا نیستند.
حل این مشکل، یک نکته کوچک را به همراه دارد. اگر href تولیدی به شکل زیر باشد:
<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>
IE حین نمایش نهایی آن، آنرا فارسی نشان خواهد داد. حتی زمانیکه کاربر بر روی آن کلیک کند، به صورت خودکار کاراکترهایی را که لازم است encode نماید، به نحو صحیحی در URL نهایی قابل مشاهده در نوار آدرسها ظاهر خواهد کرد. برای مثال %20 را به صورت خودکار اضافه میکند و نگرانی از این لحاظ وجود نخواهد داشت که الان بین دو کلمه فاصلهای وجود دارد یا خیر (مرورگرهای دیگر هم دقیقا همین رفتار را در مورد لینکهای داخل صفحه دارند).
خلاصه این توضیحات متد کمکی زیر است:
@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}
و برای نمونه نحوه استفاده از آن به شکل زیر خواهد بود:
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}
ضمن اینکه باید درنظر داشت کلا این نوع طراحی مشکل دارد! برای مثال فرض کنید که در این مثال، جزئیات، نمایش دهنده مطلب ارسالی در یک بلاگ است. یعنی یک سری عنوان و جزئیات متناظر با آنها در دیتابیس وجود دارند. اگر آدرس مطالب به این شکل باشد http://site/blog/details/text، به این معنا است که این text مساوی است با primary key جدول بانک اطلاعاتی. یعنی وبلاگ نویس سایت شما فقط یکبار در طول عمر این برنامه میتواند بگوید «سال نو مبارک!». دفعهی بعد به علت تکراری بودن، مجاز به ارسال پیام تبریک دیگری نخواهد بود! به همین جهت بهتر است طراحی را به این شکل تغییر دهید http://site/blog/details/id/text. در اینجا id همان primary key خواهد بود. Text هم عنوان مطلب. Id به جهت خوشایند بانک اطلاعاتی و Text هم برای خوشایند موتورهای جستجو در این URL قرار دارند. مطابق تعاریف مسیریابی برنامه، Text فقط حالت تزئینی داشته و پردازش نخواهد شد.
از این نوع ترفندها زیاد به کار برده میشوند. برای نمونه به URL مطالب انجمنهای معروف اینترنتی دقت کنید. عموما یک عدد را به همراه text مشاهده میکنید. عدد در برنامه پردازش میشود، متن هم برای موتورهای جستجو درنظر گرفته شده است.
پَرباد چیست؟
آنچه که شما در این مطلب یاد خواهید گرفت:
- طریقه نصب
- ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
- تایید صورتحساب
- مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
- برگشت وجه به حساب مشتری پس از تأیید صورتحساب
- درگاه مجازی پرداخت (برای تست وب اپلیکیشن، بدون داشتن حساب واقعی در درگاههای بانکی)
- تنظیمات
- ذخیره سازی اطلاعات پرداخت
طریقه نصب
PM> Install-Package Parbad
PM> Install-Package Parbad.MVC5
ایجاد صورتحساب و ارسال کاربر به درگاه پرداخت
var invoice = new Invoice( [Order Number], [Amount], [Verify URL]);
var invoice = new Invoice(1, 30000, "http://www.mywebsite.com/payment/verify" );
var result = Payment.Request(Gateways.Mellat, invoice);
if (result.Status == RequestResultStatus.Success) { // این متد، کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند result.Process(Context); } else { // در صورت تمایل میتوانید پیغام مورد نظر از درگاه پرداخت را نمایش دهید var msg = result.Message; }
در وب سایتهای MVC میتوانید به روش زیر عمل کنید
if (result.Status == RequestResultStatus.Success) { // کاربر را به سمت وب سایت درگاه پرداخت هدایت میکند return new RequestActionResult(result); } else { return View("Error"); }
تأیید صورتحساب
var result = Payment.Verify(System.Web.HttpContext.Current);
if(result.Status == VerifyResultStatus.Success) { // کاربر، صورتحساب را پرداخت کرده است و شما میتوانید ادامه عملیات خرید را انجام دهید } else { // کاربر بنا به دلایلی صورتحساب را پرداخت نکرده است // شما همچنین میتوانید علت را در قالب یک پیام از پراپرتی پیام مشاهده کنید // بنابراین شما میتوانید این صورتحساب را در پایگاه داده خود مردود اعلام کنید }
مردود کردن صورتحساب قبل از انتقال وجه از مشتری به فروشنده
همانطور که در تصویر میبینید، در هنگام بازگشت مشتری به وب سایت شما و تأیید کردن صورتحساب، شما میتوانید اطلاعات تراکنش مورد نظر را که شامل، درگاه پرداخت بانکی، شماره سفارش و شماره رجوع است را دریافت کنید و سپس با استفاده از این اطلاعات، پایگاه داده خود را بررسی کرده و در صورت لزوم، متد Cancel را فراخوانی کنید. به این ترتیب به درگاه بانکی، هیچگونه تأییدیه ای اعلام نمیشود و این بدان معناست که اگر وجهی به حساب فروشگاه واریز شده باشد، پس از چند دقیقه (معمولا ۱۵ دقیقه) به حساب مشتری برگشت داده خواهد شد.
برگشت وجه به حساب مشتری پس از تأیید صورتحساب
var refundResult = Payment.Refund(new RefundInvoice([Order Number], [Amount]));
درگاه مجازی پرداخت
var result = Payment.Request(Gateways.ParbadVirtualGateway, invoice);
در نتیجه در هنگام هدایت کاربر به درگاه پرداخت، کاربر به درگاه مجازی هدایت خواهد شد.
<system.webServer> <handlers> <add name="ParbadGatewayPage" verb="*" path="Parbad.axd" type="Parbad.Web.Gateway.ParbadVirtualGatewayHandler" /> </handlers> </system.webServer>
ParbadConfiguration.Gateways.ConfigureParbadVirtualGateway(new ParbadVirtualGatewayConfiguration("Parbad.axd"));
تنظیمات پَرباد
public class Global : HttpApplication { void Application_Start(object sender, EventArgs e) { // configurations } }
public class Startup { public void Configuration(IAppBuilder app) { // configurations } }
تنظیمات درگاههای پرداخت
تنظیمات ذخیره سازی اطلاعات پرداخت
ParbadConfiguration.Storage = new SqlServerStorage("Connection String", "MyPaymentTableName");
public class MyStorage : Storage { // Implement methods here... }
ParbadConfiguration.Storage = new MyStorage();
در صورتیکه هر گونه پیشنهاد یا انتقاد نسبت به کارکرد این سیستم دارید، صمیمانه منتظر شنیدن آن در راستای توسعه این سیستم هستم.همچنین در صورت تمایل به توسعه آن، میتوانید آن را در گیت هاب دنبال کنید و یا لطفا پیشنهادات، بحثها و نظرات خود را در صفحه مخصوص این پروژه ارسال کنید.با تشکر.
Command چیست ؟
اینترفیس ICommand:
نام عضو | توضیحات |
Bool CanExecute(object parameter) | این تابع پارامتری از نوع object را دریافت میکند و یک مقدار bool را به خروجی تابع میفرستد. اگر مقدار خروجی متد، true باشدcommand اجرا خواهد شد و در غیر اینصورت اتفاقی رخ نخواهد داد. اغلب ازDelegate ها به عنوان پارامتر این تابع استفاده میشود؛Delegate های پیش ساختهای همچون Action,Predicate,Func |
Event EventHandler CanExecuteChanged | این رویداد برای آگاه سازی UI که به Command متصل است، استفاده میشود .بر اساس خروجی تابع CanExecute، این رویداد اتفاق میافتد. |
Void Execute(Object parameter) | این متد کار اصلی را در Commandها انجام میدهد. این متد تنها زمانی اجرا میشود که متدCanExecute مقدار true را بازگرداند. این تابع پارامتری را از نوع object دریافت میکند، اما عموما ما یکDelegate را به آن ارسال میکنیم. Delegate ارجاعی را به متدی، در خود نگاه میدارد که انتظار داریم در صورت اجرایcommand ، اجرا شود. |
چرا اینترفیس ICommand :
ساخت UI مورد نیاز :
گام دوم:
<Grid DataContext="{Binding Source={StaticResource calculatorVM}}" Background="#FFCCCC"> <Grid.RowDefinitions> <RowDefinition Height="80"/> <RowDefinition/> <RowDefinition Height="80"/> <RowDefinition Height="44"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4" FontSize="25" VerticalAlignment="Top" HorizontalAlignment="Center" Foreground="Blue" Content="ICommand Sample"/> <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20" Content="First Input"/> <Label Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="2" Margin="10,0,0,0" VerticalAlignment="Bottom" FontSize="20" Content="Second Input"/> <TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left" Height="30" Width="150" TextAlignment="Center" Text="{Binding FirstValue, Mode=TwoWay}"/> <TextBox Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2" Margin="10,0,0,0" FontSize="20" HorizontalAlignment="Left" Height="30" Width="150" TextAlignment="Center" Text="{Binding SecondValue, Mode=TwoWay}"/> <Rectangle Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4" Fill="LightBlue"/> <Button Grid.Row="2" Grid.Column="0" Content="+" Margin="10,0,0,0" HorizontalAlignment="Left" Height="50" Width="50" FontSize="30" Command="{Binding AddCommand}"/> <Label Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" FontSize="25" Margin="10,0,0,0" HorizontalAlignment="Left" Height="50" Content="Result : "/> <TextBlock Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="2" FontSize="20" Margin="10,0,0,0" Background="BlanchedAlmond" TextAlignment="Center" HorizontalAlignment="Left" Height="36" Width="150" Text="{Binding Output}"/> </Grid>
xmlns:myview="clr-namespace:ICommnadSample.Views"
<Grid> <myview:CalculatorView/> </Grid>
گام سوم :
private double firstValue; private double secondValue; private double output; public double FirstValue { get { return firstValue; } set { firstValue = value; OnPropertyChanged("FirstValue"); } } public double SecondValue { get { return secondValue; } set { secondValue = value; OnPropertyChanged("SecondValue"); } } public double Output { get { return output; } set { output = value; OnPropertyChanged("Output"); } }
گام چهارم:
public class PlusCommand : ICommand { private CalculatorViewModel calculatorViewModel; public PlusCommand(CalculatorViewModel vm) { calculatorViewModel = vm; } public bool CanExecute(object parameter) { return true; } public void Execute(object parameter) { calculatorViewModel.Add(); } public event EventHandler CanExecuteChanged; }
گام پنجم:
private PlusCommand plusCommand; public CalculatorViewModel() { plusCommand = new PlusCommand(this); }
گام ششم:
xmlns:vm="clr-namespace:ICommnadSample.ViewModels"
<UserControl.Resources> <vm:CalculatorViewModel x:Key="calculatorVM" /> </UserControl.Resources>
گام هفتم:
public void Add() { Output = firstValue + secondValue; }
گام هشتم:
public ICommand AddCommand { get { return plusCommand; } }
نحوهی استفاده:
Command="{Binding AddCommand}"
public class ViewModelBase:INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
گام دهم:
public class CalculatorViewModel : ViewModelBase
OnPropertyChanged("FirstValue");
public class CalculatorViewModel : ViewModelBase { private double firstValue; private double secondValue; private double output; private PlusCommand plusCommand; public CalculatorViewModel() { plusCommand = new PlusCommand(this); } public double FirstValue { get { return firstValue; } set { firstValue = value; OnPropertyChanged("FirstValue"); } } public double SecondValue { get { return secondValue; } set { secondValue = value; OnPropertyChanged("SecondValue"); } } public double Output { get { return output; } set { output = value; OnPropertyChanged("Output"); } } public ICommand AddCommand { get { return plusCommand; } } internal void Add() { Output = firstValue + secondValue; } }
حال میتوانید برنامه را اجرا و تست کنید:
برای درک بهتر عملیات انقیاد دادها میتوانید به این مقاله مراجعه کنید.
هدف: استفاده از کتابخانهی jsSHA
میخواهیم در یک برنامهی AngularJS 2.0، از کتابخانهی jsSHA استفاده کرده و هش SHA512 یک رشته را محاسبه کنیم.
تامین پیشنیازهای اولیه
میتوان فایلهای این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است اینکار را توسط npm مدیریت کنیم. به همین جهت فایل package.json آنرا گشوده و سپس مدخل متناظری را به آن اضافه کنید:
"dependencies": { // ... "jssha": "^2.1.0", // ... },
بارگذاری فایلهای کتابخانه به صورت پویا
یک روش استفاده از این کتابخانه یا هر کتابخانهی جاوا اسکریپتی، افزودن مدخل تعریف آن به صفحهی index.html است:
<script src="node_modules/jssha/src/sha512.js"></script>
// map tells the System loader where to look for things var map = { // ... 'jssha': 'node_modules/jssha/src' }; // packages tells the System loader how to load when no filename and/or no extension var packages = { // ... 'jssha': { main: 'sha512.js', defaultExtension: 'js' } };
به این ترتیب هر زمانیکه کار import این کتابخانه صورت گیرد، بارگذاری پویای آن انجام خواهد شد. به علاوه ابزارهای بسته بندی و deploy پروژه هم این فایل را پردازش کرده و به صورت خودکار، کار bundling، فشرده سازی و یکی سازی اسکریپتها را انجام میدهند.
استفاده از jsSHA به صورت untyped
پس از دریافت بستههای این کتابخانه و مشخص سازی نحوهی بارگذاری پویای آن، اکنون نوبت به استفادهی از آن است. در اینجا منظور از untyped این است که فرض کنیم برای این کتابخانه، فایلهای typings مخصوص TypeScript وجود ندارند و پس از جستجوی در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped نتوانستهایم معادلی را برای آن پیدا کنیم. بنابراین فایل جدید untyped-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core'; var jsSHA = require("jssha"); // ==> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions //declare var jsSHA: any; // ==> this requires adding <script src="node_modules/jssha/src/sha512.js"></script> to the first page manually. @Component({ templateUrl: 'app/using-third-party-libraries/untyped-sha.component.html' }) export class UnTypedShaComponent implements OnInit { hash: String; ngOnInit(): void { let shaObj = new jsSHA("SHA-512", "TEXT"); shaObj.update("This is a test"); this.hash = shaObj.getHash("HEX"); } }
<h1>SHA-512 Hash / UnTyped</h1> <p>String: This is a test</p> <p>HEX: {{hash}}</p>
هر زمانیکه فایلهای typing یک کتابخانهی جاوا اسکریپتی در دسترس نبودند، فقط کافی است از روش declare var jsSHA: any استفاده کنید. در اینجا any به همان حالت استاندارد و بینوع جاوا اسکریپت اشاره میکند. در این حالت برنامه بدون مشکل کامپایل خواهد شد؛ اما از تمام مزایای TypeScript مانند بررسی نوعها و همچنین intellisense محروم میشویم.
در این مثال در hook ویژهای به نام OnInit، کار ساخت شیء SHA را انجام داده و سپس هش عبارت This is a test محاسبه شده و به خاصیت عمومی hash انتساب داده میشود. سپس این خاصیت عمومی، در قالب این کامپوننت از طریق روش interpolation نمایش داده شدهاست.
دو نکتهی مهم
الف) اگر از روش declare var jsSHA: any استفاده کردید، کار بارگذاری فایل sha512.js به صورت خودکار رخ نخواهد داد؛ چون ماژولی را import نمیکند. بنابراین تعاریف systemjs.config.js ندید گرفته خواهد شد. در این حالت باید از همان روش متداول افزودن تگ script این کتابخانه به فایل index.html استفاده کرد.
ب) برای بارگذاری پویای کتابخانهی jsSHA بر اساس تعاریف فایل systemjs.config.js از متد require کمک بگیرید:
var jsSHA = require("jssha");
استفاده از jsSHA به صورت typed
کتابخانهی jsSHA در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jssha دارای فایل d.ts. مخصوص خود است. برای نصب آن از یکی از دو روش ذیل استفاده کنید:
الف) نصب دستی فایلهای typings
npm install -g typings typings install jssha --save --ambient
ب) تکمیل فایل typings.ts
{ "ambientDependencies": { // ... "jssha": "registry:dt/jssha#2.1.0+20160317120654" } }
پس از نصب فایلهای typings این پروژه، به فایل main.ts مراجعه کرده و مدخل ذیل را به ابتدای آن اضافه کنید:
/// <reference path="../typings/browser/ambient/jssha/index.d.ts" />
در ادامه فایل جدید typed-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core'; //import { jsSHA } from "jssha"; import * as jsSHA from "jssha"; // ===> var jsSHA = require("jssha"); // ===> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions @Component({ templateUrl: 'app/using-third-party-libraries/typed-sha.component.html' }) export class TypedShaComponent implements OnInit{ hash: String; ngOnInit(): void { let shaObj = new jsSHA("SHA-512", "TEXT"); shaObj.update("This is a test"); this.hash = shaObj.getHash("HEX"); } }
در اینجا تنها نکتهی مهم و جدید نسبت به روش قبل (استفاده از jsSHA به صورت untyped)، روش import این کتابخانه است. روش "import * as jsSHA from "jssha به عبارت var jsSHA = require("jssha") ترجمه میشود که در نهایت سبب بارگذاری خودکار فایلهای jssha بر اساس تعاریف مداخل آن در فایل systemjs.config.js میگردد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید.
PM> Install-Package DNTFrameworkCore.Web
- Refactor کردن فرمهای ثبت و ویرایش مرتبط با یک Aggregate، به یک PartialView که با یک ViewModel کار میکند. برای موجودیتهای ساده و پایه، همان Model/DTO، به عنوان Model متناظر با یک ویو یا به اصطلاح ViewModel استفاده میشود؛ ولی برای سایر موارد، از مدلی که نام آن با نام موجودیت + کلمه ModalViewModel یا FormViewModel تشکیل میشود، استفاده خواهیم کرد.
- یک فرم، در قالب یک پارشالویو، به صورت Ajaxای با استفاده از افزونه jquery-unobtrusive-ajax بارگذاری شده و به سرور ارسال خواهد شد.
- یک فرم براساس طراحی خود میتواند در قالب یک مودال باز شود، یا به منظور inline-editing آن را بارگذاری و به قسمتی از صفحه که مدنظرتان میباشد اضافه شود.
- وجود ویو Index به همراه پارشالویو _List برای نمایش لیستی و یک پارشالویو برای عملیات ثبت و ویرایش الزامی میباشد. البته اگر از مکانیزمی که در مطلب « طراحی یک گرید با jQuery Ajax و ASP.NET MVC به همراه پیاده سازی عملیات CRUD» مطرح شد، استفاده نمیکنید و نیاز دارید تا اطلاعات صفحهبندی شده، مرتب شده و فیلتر شدهای را در قالب JSON دریافت کنید، از اکشنمتد ReadPagedList کنترلر پایه استفاده کنید.
public class BlogsController : CrudController<IBlogService, int, BlogModel> { public BlogsController(IBlogService service) : base(service) { } protected override string CreatePermissionName => PermissionNames.Blogs_Create; protected override string EditPermissionName => PermissionNames.Blogs_Edit; protected override string ViewPermissionName => PermissionNames.Blogs_View; protected override string DeletePermissionName => PermissionNames.Blogs_Delete; protected override string ViewName => "_BlogModal"; }
<form asp-action="@(Model.IsNew() ? "Create" : "Edit")" asp-controller="Blogs" asp-modal-form="BlogForm"> <div> <input type="hidden" name="save-continue" value="true"/> <input asp-for="RowVersion" type="hidden"/> <input asp-for="Id" type="hidden"/> <div> <div> <label asp-for="Title"></label> <input asp-for="Title" autocomplete="off"/> <span asp-validation-for="Title"></span> </div> </div> <div> <div> <label asp-for="Url"></label> <input asp-for="Url" type="url"/> <span asp-validation-for="Url"></span> </div> </div> </div> ... </form>
<div> <a asp-modal-delete-link asp-model-id="@Model.Id" asp-modal-toggle="false" asp-controller="Blogs" asp-action="Delete" asp-if="!Model.IsNew()" asp-permission="@PermissionNames.Blogs_Delete" title="Delete Blog"> <i></i> </a> <a title="Refresh Blog" asp-if="!Model.IsNew()" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Edit" asp-route-id="@Model.Id"> <i></i> </a> <a title="New Blog" asp-modal-link asp-modal-toggle="false" asp-controller="Blogs" asp-action="Create"> <i></i> </a> <button type="button" data-dismiss="modal"> <i></i> Cancel </button> <button type="submit"> <i></i> Save Changes </button> </div>
public class RoleModalViewModel : RoleModel { public IReadOnlyList<LookupItem> PermissionList { get; set; } }
protected override IActionResult RenderView(RoleModel role) { var model = _mapper.Map<RoleModalViewModel>(role); model.PermissionList = ReadPermissionList(); return PartialView(ViewName, model); }
برای مدیریت سناریوهای Master-Detail به مانند قسمت مدیریت دسترسیها در تب Permissions فرم بالا، امکاناتی در زیرساخت تعبیه شده است ولی پیادهسازی آن را به عنوان یک تمرین و با توجه به سری مطالب «Editing Variable Length Reorderable Collections in ASP.NET MVC» به شما واگذار میکنم.
نکته تکمیلی: برای ارسال اطلاعات اضافی به ویو Index متناظر با یک موجودیت میتوانید متد RenderIndex را به شکل زیر بازنویسی کنید:
protected override IActionResult RenderIndex(IPagedQueryResult<RoleReadModel> model) { var indexModel = new RoleIndexViewModel { Items = model.Items, TotalCount = model.TotalCount, Permissions = ReadPermissionList() }; return Request.IsAjaxRequest() ? (IActionResult) PartialView(indexModel) : View(indexModel); }
مدل RoleIndexViewModel استفاده شده در تکه کد بالا نیز به شکل زیر خواهد بود:
public class RoleIndexViewModel : PagedQueryResult<RoleReadModel> { public IReadOnlyList<LookupItem> Permissions { get; set; } }
فرآیند بارگذاری یک پارشالویو در مودال
به عنوان مثال برای استفاده از مودالهای بوت استرپ، ایده کار به این شکل است که یک مودال را به شکل زیر در فایل Layout قرار دهید:
<div class="modal fade" @*tabindex="-1"*@ id="main-modal" data-keyboard="true" data-backdrop="static" role="dialog" aria-hidden="true"> <div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-content"> <div class="modal-body"> Loading... </div> </div> </div> </div>
سپس در زمان کلیک بروی یک دکمه Ajaxای، ابتدا main-modal را نمایش داده و بعد از دریافت پارشالویو از سرور، آن را با محتوای modal-content جایگزین میکنیم. به همین دلیل Tag Halperهای مطرح شده در مطلب جاری، callbackهای failure/complete/success متناظر با unobtrusive-ajax را نیز مقداردهی میکنند. برای این منظور نیاز است تا متدهای جاوااسکریپتی زیر نیز در سطح شیء window تعریف شده باشند:
/*---------------------------------- asp-modal-link ---------------------------*/ window.handleModalLinkLoaded = function (data, status, xhr) { prepareForm('#main-modal.modal form'); }; window.handleModalLinkFailed = function (xhr, status, error) { //.... }; /*---------------------------------- asp-modal-form ---------------------------*/ window.handleModalFormBegin = function (xhr) { $('#main-modal a').addClass('disabled'); $('#main-modal button').attr('disabled', 'disabled'); }; window.handleModalFormComplete = function (xhr, status) { $('#main-modal a').removeClass('disabled'); $('#main-modal button').removeAttr('disabled'); }; window.handleModalFormSucceeded = function (data, status, xhr) { if (xhr.getResponseHeader('Content-Type') === 'text/html; charset=utf-8') { prepareForm('#main-modal.modal form'); } else { hideMainModal(); } }; window.handleModalFormFailed = function (xhr, status, error, formId) { if (xhr.status === 400) { handleBadRequest(xhr, formId); } };
برای بررسی بیشتر، پیشنهاد میکنم پروژه DNTFrameworkCore.TestWebApp موجود در مخزن این زیرساخت را بازبینی کنید.
// Old code: app.UseSignalR(routes => { routes.MapHub<SomeHub>("/path"); }); // New code: app.UseEndpoints(endpoints => { endpoints.MapHub<SomeHub>("/path"); });
// Old code: app.UseSignalR(routes => { routes.MapHub<SomeHub>("/path"); }); // New code: app.UseEndpoints(endpoints => { endpoints.MapHub<SomeHub>("/path"); });
چند نکته اضافه برای Refactoring
public bool SaveTextF(string path) { if (_isTextChanged) { if (string.IsNullOrWhiteSpace(path)) { using (var dlg = new SaveFileDialog {Filter = "*.txt"}) { if (dlg.ShowDialog() == DialogResult.OK) path = dlg.FileName; else return false; } } //call save methods on path } return true; }
public bool SaveTextT(string path) { if (!_isTextChanged) return true; if (string.IsNullOrWhiteSpace(path)) using (var dlg = new SaveFileDialog {Filter = "*.txt"}) { if (dlg.ShowDialog() != DialogResult.OK) return false; path = dlg.FileName; } //call save methods on path return true; }
ssh://username@mygitserver.com/path/myrepo.git
https://username@mygitserver.com/path/myrepo.git