تبدیل عدد به حروف
- چون عموما از عدد به حروف در گزارشات مالی استفاده میشود، اعداد همه int و big int هستند. بنابراین آنچنان کاربرد دنیای واقعی ندارد سایر حالتها.
- بحث اضافه کردن سایر زبانها ... خوب، بستگی به تسلط به زبانهای مختلف هم دارد. مثلا در انگلیسی میگویند Three hundred اما در فارسی مرسوم نیست که کسی بگه «سه صد». به همین جهت یک قسمت اضافهتر برای معرفی سیصد و امثال آن در کد فوق وجود دارد. به احتمال زیاد زبانهای دیگر هم ریزهکاریهای خاص خودشان را دارند.
- بحث سرعت را هم در نظر بگیرید. در این نوع الگوریتمها به علت استفاده مکرر، ترجیح داده میشود که از کالکشنهای تشکیل شده در حافظه (بجای خواندن از فایل) جهت سرعت بالاتر دسترسی به اطلاعات و سربار کمتر استفاده شود.
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 مشاهده میکنید. عدد در برنامه پردازش میشود، متن هم برای موتورهای جستجو درنظر گرفته شده است.
در قسمت قبلی بروز رسانی موجودیتهای منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه میتوان تغییرات موجودیتها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.
تشخیص تغییرات با Web API
فرض کنید میخواهیم از سرویسهای Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر میخواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت دادهها هم با مدل Code-First پیاده سازی میشود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی میکند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما آژانسهای مسافرتی و رزرواسیون آنها را ارائه میکند. میخواهیم مدل و کد دسترسی دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
- کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
- دو کلاس جدید با نامهای TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent { public TravelAgent() { this.Bookings = new HashSet<Booking>(); } public int AgentId { get; set; } public string Name { get; set; } public virtual ICollection<Booking> Bookings { get; set; } } public class Booking { public int BookingId { get; set; } public int AgentId { get; set; } public string Customer { get; set; } public DateTime BookingDate { get; set; } public bool Paid { get; set; } public virtual TravelAgent TravelAgent { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext { public Recipe3Context() : base("Recipe3ConnectionString") { } public DbSet<TravelAgent> TravelAgents { get; set; } public DbSet<Booking> Bookings { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId); modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents"); modelBuilder.Entity<Booking>().ToTable("Bookings"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings> <add name="Recipe3ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال میکند. همچنین به JSON serializer میگوییم که self-referencing loop خاصیتهای پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیتها بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "ActionMethodSave", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }); }
- در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController { // GET api/travelagent [HttpGet] public IEnumerable<TravelAgent> Retrieve() { using (var context = new Recipe3Context()) { return context.TravelAgents.Include(x => x.Bookings).ToList(); } } /// <summary> /// Update changes to TravelAgent, implementing Action-Based Routing in Web API /// </summary> public HttpResponseMessage Update(TravelAgent travelAgent) { using (var context = new Recipe3Context()) { var newParentEntity = true; // adding the object graph makes the context aware of entire // object graph (parent and child entities) and assigns a state // of added to each entity. context.TravelAgents.Add(travelAgent); if (travelAgent.AgentId > 0) { // as the Id property has a value greater than 0, we assume // that travel agent already exists and set entity state to // be updated. context.Entry(travelAgent).State = EntityState.Modified; newParentEntity = false; } // iterate through child entities, assigning correct state. foreach (var booking in travelAgent.Bookings) { if (booking.BookingId > 0) // assume booking already exists if ID is greater than zero. // set entity to be updated. context.Entry(booking).State = EntityState.Modified; } context.SaveChanges(); HttpResponseMessage response; // set Http Status code based on operation type response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent); return response; } } [HttpDelete] public HttpResponseMessage Cleanup() { using (var context = new Recipe3Context()) { context.Database.ExecuteSqlCommand("delete from [bookings]"); context.Database.ExecuteSqlCommand("delete from [travelagents]"); } return Request.CreateResponse(HttpStatusCode.OK); } }
- در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program { private HttpClient _client; private TravelAgent _agent1, _agent2; private Booking _booking1, _booking2, _booking3; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up is completed await program.CleanupAsync(); program.CreateFirstAgent(); // do not proceed until agent is created await program.AddAgentAsync(); program.CreateSecondAgent(); // do not proceed until agent is created await program.AddSecondAgentAsync(); program.ModifyAgent(); // do not proceed until agent is updated await program.UpdateAgentAsync(); // do not proceed until agents are fetched await program.FetchAgentsAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")}; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } private async Task CleanupAsync() { // call cleanup method in service _response = await _client.DeleteAsync("api/travelagent/cleanup/"); } private void CreateFirstAgent() { // create new Travel Agent and booking _agent1 = new TravelAgent {Name = "John Tate"}; _booking1 = new Booking { Customer = "Karen Stevens", Paid = false, BookingDate = DateTime.Parse("2/2/2010") }; _booking2 = new Booking { Customer = "Dolly Parton", Paid = true, BookingDate = DateTime.Parse("3/10/2010") }; _agent1.Bookings.Add(_booking1); _agent1.Bookings.Add(_booking2); } private async Task AddAgentAsync() { // call generic update method in Web API service to add agent and bookings _response = await _client.PostAsync("api/travelagent/update/", _agent1, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include // database-generated Ids for each entity _agent1 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens"); _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void CreateSecondAgent() { // add new agent and booking _agent2 = new TravelAgent {Name = "Perry Como"}; _booking3 = new Booking { Customer = "Loretta Lynn", Paid = true, BookingDate = DateTime.Parse("3/15/2010")}; _agent2.Bookings.Add(_booking3); } private async Task AddSecondAgentAsync() { // call generic update method in Web API service to add agent and booking _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service _agent2 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent2.Name, _agent2.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void ModifyAgent() { // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1 _agent2.Name = "Perry Como, Jr."; _agent2.Bookings.Add(_booking1); } private async Task UpdateAgentAsync() { // call generic update method in Web API service to update agent 2 _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result; Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private async Task FetchAgentsAsync() { // call Get method on service to fetch all Travel Agents and Bookings _response = _client.GetAsync("api/travelagent/retrieve").Result; if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>(); foreach (var agent in agents) { Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count()); } } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } }
- در آخر کلاسهای TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژهها به اشتراک گذاشته شوند.
اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.
(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت میکنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی میکنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام دادههای پیشین را حذف میکند.
در قدم بعدی سه آبجکت جدید میسازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکتها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال میکنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت میکند و آن را به موجودیت TravelAgents در Context جاری اضافه مینماید. این کار آبجکت آژانس مسافرتی و تمام آبجکتهای فرزند آن را در حالت Added اضافه میکند و باعث میشود که context جاری شروع به ردیابی (tracking) آنها کند.
نکته: قابل ذکر است که اگر موجودیتهای متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکتهای خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر میکند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.
بعد از آن بر اساس مقدار خاصیت Id مشخص میکنیم که موجودیتها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر میدهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها میکنیم.
سپس تمام آبجکتهای فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال میکنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر میدهیم. در نهایت متد SaveChanges را فراخوانی میکنیم. در این مرحله برای موجودیتهای جدید اسکریپتهای Insert و برای موجودیتهای تغییر کرده اسکریپتهای Update تولید میشود. سپس کد وضعیت مناسب را به کلاینت بر میگردانیم. برای موجودیتهای اضافه شده کد وضعیت 201 (Created) و برای موجودیتهای بروز رسانی شده کد وضعیت 200 (OK) باز میگردد. کد 201 به کلاینت اطلاع میدهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر میدهد. هنگام تولید سرویسهای REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.
پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی میسازیم و آنها را به سرویس ارسال میکنیم. سپس نام آژانس مسافرتی دوم را تغییر میدهیم، و یکی از رزرواسیونها را از آژانس اولی به آژانس دومی منتقل میکنیم. اینبار هنگام فراخوانی متد Update تمام موجودیتها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر میدهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.
در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی میکند. این فراخوانی با کمک متد GetAsync انجام میشود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانسهای مسافرتی بهمراه رزرواسیونهای متناظرشان را دریافت میکند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری میکنیم.
دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز میگرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.
نکته دیگر آنکه در مثال جاری از قراردادهای توکار Web API برای نگاشت درخواستهای HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواستهای POST به متدهایی نگاشت میشوند که نام آنها با "Post" شروع میشود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشنهای واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویسهای REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواستهای متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.
((DateTimeOffset)value).Ticks.ToString()
namespace MySQLite { public class SQLiteDbContext : DbContext { public SQLiteDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); addDateTimeOffsetConverter(builder); } private static void addDateTimeOffsetConverter(ModelBuilder builder) { // SQLite does not support DateTimeOffset foreach (var property in builder.Model.GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof(DateTimeOffset))) { property.SetValueConverter( new ValueConverter<DateTimeOffset, DateTime>( convertToProviderExpression: dateTimeOffset => dateTimeOffset.UtcDateTime, convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime) )); } foreach (var property in builder.Model.GetEntityTypes() .SelectMany(t => t.GetProperties()) .Where(p => p.ClrType == typeof(DateTimeOffset?))) { property.SetValueConverter( new ValueConverter<DateTimeOffset?, DateTime>( convertToProviderExpression: dateTimeOffset => dateTimeOffset.Value.UtcDateTime, convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime) )); } } } }
// یک نمونهی دیگر private static readonly ValueConverter<object, string> DateTimeOffsetToStringConverter = new ValueConverter<object, string>( v => ((DateTimeOffset)v).ToString(@"yyyy\-MM\-dd HH\:mm\:ss.FFFFFFFzzz", CultureInfo.InvariantCulture), v => DateTimeOffset.Parse(v, CultureInfo.InvariantCulture));
با استفاده از AutoComplete TextBoxes میتوان گوشهای از زندگی روزمرهی کاربران یک برنامه را سادهتر کرد. مشکل مهم dropDownList ها دریک برنامهی وب، عدم امکان تایپ قسمتی از متن مورد نظر و سپس نمایان شدن آیتمهای متناظر با آن در اسرع وقت میباشد. همچنین با تعداد بالای آیتمها هم حجم صفحه و زمان بارگذاری را افزایش میدهند. راه حلهای بسیار زیادی برای حل این مشکل وجود دارند و یکی از آنها ایجاد AutoComplete TextBoxes است. پلاگینهای متعددی هم جهت پیاده سازی این قابلیت نوشته شدهاند منجمله jQuery Autocomplete . این پلاگین دیگر توسط نویسندهی اصلی آن نگهداری نمیشود اما توسط برنامه نویسی دیگر در github ادامه یافته است. در ادامه نحوهی استفاده از این افزونه را در ASP.NET Webforms بررسی خواهیم کرد.
الف) دریافت افزونه
لطفا به آدرس GitHub ذکر شده مراجعه نمائید.
سپس برای مثال پوشهی js را به پروژه افزوده و فایلهای jquery-1.5.min.js ، jquery.autocomplete.js ، jquery.autocomplete.css و indicator.gif را در آن کپی کنید. فایل indicator.gif به همراه مجموعهی دریافتی ارائه نمیشود و یک آیکن loading معروف میتواند باشد.
علاوه بر آن یک فایل جدید custom.js را نیز جهت تعاریف سفارشی خودمان اضافه خواهیم کرد.
ب) افزودن تعاریف افزونه به صفحه
در ذیل نحوهی افزودن فایلهای فوق به یک master page نمایش داده شده است.
در اینجا از قابلیتهای جدید ScriptManager (موجود در سرویس پک یک دات نت سه و نیم و یا دات نت چهار) جهت یکی کردن اسکریپتها کمک گرفته شده است. به این صورت تعداد رفت و برگشتها به سرور بهجای سه مورد (تعداد فایلهای اسکریپت مورد استفاده)، یک مورد (نهایی یکی شده) خواهد بود و همچنین حاصل نهایی به صورت خودکار به شکلی فشرده شده به مرورگر تحویل داده شده، سرآیندهای کش شدن اطلاعات به آن اضافه میگردد (که در سایر حالات متداول اینگونه نیست)؛ به علاوه Url نهایی آن هم بر اساس hash فایلها تولید میشود. یعنی اگر محتوای یکی از این فایلها تغییر کرد، چون Url نهایی تغییر میکند، دیگر لازم نیست نگران کش شدن و به روز نشدن اسکریپتها در سمت کاربر باشیم.
<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="AspNetjQueryAutocompleteTest.Site" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:PlaceHolder Runat="server">
<link href="<%= ResolveClientUrl("~/js/jquery.autocomplete.css")%>" rel="stylesheet" type="text/css" />
</asp:PlaceHolder>
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
<CompositeScript>
<Scripts>
<asp:ScriptReference Path="~/js/jquery-1.5.min.js" />
<asp:ScriptReference Path="~/js/jquery.autocomplete.js" />
<asp:ScriptReference Path="~/js/custom.js" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>
<div>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
ج) افزودن یک صفحهی ساده به برنامه
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="default.aspx.cs" Inherits="AspNetjQueryAutocompleteTest._default" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<asp:TextBox ID="txtShenas" runat="server" />
</asp:Content>
فرض کنید میخواهیم افزونهی ذکر شده را به TextBox استاندارد فوق اعمال کنیم. ID این TextBox در نهایت به شکل ContentPlaceHolder1_txtShenas رندر خواهد شد. البته در ASP.NET 4.0 با تنظیم ClientIDMode=Static میتوان ID انتخابی خود را به جای این ID خودکار درنظر گرفت و اعمال کرد. اهمیت این مساله در قسمت (ه) مشخص میگردد.
د) فراهم آوردن اطلاعات مورد استفاده توسط افزونهی AutoComplete به صورت پویا
مهمترین قسمت استفاده از این افزونه، تهیهی اطلاعاتی است که باید نمایش دهد. این اطلاعات باید به صورت فایلی که هر سطر آن حاوی یکی از آیتمهای مورد نظر است، تهیه گردد. برای این منظور میتوان از فایلهای ASHX یا همان Generic handlers استفاده کرد:
using System;
using System.Data.SqlClient;
using System.Text;
using System.Web;
namespace AspNetjQueryAutocompleteTest
{
public class AutoComplete : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string prefixText = context.Request.QueryString["q"];
var sb = new StringBuilder();
using (var conn = new SqlConnection())
{
//todo: این مورد باید از فایل کانفیگ خوانده شود
conn.ConnectionString = "Data Source=(local);Initial Catalog=MyDB;Integrated Security = true";
using (var cmd = new SqlCommand())
{
cmd.CommandText = @" select Field1 ,Field2 from tblData where Field1 like @SearchText + '%' ";
cmd.Parameters.AddWithValue("@SearchText", prefixText);
cmd.Connection = conn;
conn.Open();
using (var sdr = cmd.ExecuteReader())
{
if (sdr != null)
while (sdr.Read())
{
string field1 = sdr.GetValue(0) == DBNull.Value ? string.Empty : sdr.GetValue(0).ToString().Trim();
string field2 = sdr.GetValue(1) == DBNull.Value ? string.Empty : sdr.GetValue(1).ToString().Trim();
sb.AppendLine(field1 + "|" + field2);
}
}
}
}
context.Response.Write(sb.ToString());
}
public bool IsReusable
{
get
{
return false;
}
}
}
}
در این مثال از ADO.NET کلاسیک استفاده شده است تا به عمد نحوهی تعریف پارامترها یکبار دیگر مرور گردند. اگر از LINQ to SQL یا Entity framework یا NHibernate و موارد مشابه استفاده میکنید، جای نگرانی نیست؛ زیرا کوئریهای SQL تولیدی توسط این ORMs به صورت پیش فرض از نوع پارامتری هستند (+).
در این مثال اطلاعات دو فیلد یک و دوی فرضی از جدولی با توجه به استفاده از like تعریف شده دریافت میگردد. به عبارتی همان متد StartsWith معروف LINQ بکارگرفته شده است.
به صورت خلاصه افزونه، کوئری استرینگ q را به این فایل ashx ارسال میکند. سپس کلیه آیتمهای شروع شده با مقدار دریافتی، از بانک اطلاعاتی دریافت شده و هر کدام قرارگرفته در یک سطر جدید بازگشت داده میشوند.
اگر دقت کرده باشید در قسمت sb.AppendLine ، با استفاده از "|" دو مقدار دریافتی از هم جدا شدهاند. عموما یک مقدار کفایت میکند (در 98 درصد موارد) ولی اگر نیاز بود تا توضیحاتی نیز نمایش داده شود از این روش نیز میتوان استفاده کرد. برای مثال یک مقدار خاص به همراه توضیحات آن به عنوان یک آیتم نمایش داده شده مد نظر است.
ه) اعمال نهایی افزونه به TextBox
در ادامه پیاده سازی فایل custom.js برای استفاده از امکانات فراهم شده در قسمتهای قبل ارائه گردیده است:
function formatItem(row) {
return row[0] + "<br/><span style='text-align:justify;' dir='rtl'>" + row[1] + "</span>";
}
$(document).ready(function () {
$("#ContentPlaceHolder1_txtShenas").autocomplete('AutoComplete.ashx', {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
delay: 5,
//Only suggested values are valid
mustMatch: true,
//The number of items in the select box
max: 20,
//Fill the input while still selecting a value
autoFill: false,
//The comparison doesn't looks inside
matchContains: false,
formatItem: formatItem
});
});
پس از این مقدمات، اعمال افزونهی autocomplete به textBox ایی با id مساوی ContentPlaceHolder1_txtShenas ساده است. اطلاعات از فایل AutoComplete.ashx دریافت میگردد و تعدادی از خواص پیش فرض این افزونه در اینجا مقدار دهی شدهاند. لیست کامل آنها را در فایل jquery.autocomplete.js میتوان مشاهده کرد.
تنها نکتهی مهم آن استفاده از پارامتر اختیاری formatItem است. اگر در حین تهیهی AutoComplete.ashx خود تنها یک آیتم را در هر سطر نمایش میدهید و از "|" استفاده نکردهاید، نیازی به ذکر آن نیست. در این مثال ویژه، فیلد یک در یک سطر و فیلد دو در سطر دوم یک آیتم نمایش داده میشوند:
Func یا Expression Func در EF
حتی آقای نصیری هم به همین صورت استفاده کرده اند.
public virtual TEntity Find(Expression<Func<TEntity, bool>> predicate)
public virtual TEntity Find(Expression<Predicate<TEntity>> predicate)
اضافه شدن SHA1 به SQLite 3.17.0
بررسی علت CPU Usage بالای برنامه در حال اجرا
- در سرور 32 بیتی شما نمیتونید استفاده مناسبی از سخت افزاری که تهیه کردید بکنید. نیاز است به سرور 64 بیتی نقل مکان کنید. یعنی هزینه کردید اما ... از آن استفاده بهینهای نمیکنید.
- تابع SQL ، برای تبدیل تاریخ میلادی به شمسی وجود دارد (بگردید در انجمنها هست). از همان تابع در کوئریهای خودتون قبل از ارسال به گزارشات استفاده کنید. یعنی دیتا از همان اول تاریخ شمسی داشته باشد. به این صورت نیازی به استفاده از افزونه دیگری نیست.
- IIS یک قسمت دارد به نام Application pool . اولا به ازای هر برنامه بهتر است یک Application pool جدا درست کنید. این حالت دقیقا مثل اجرای هر سایت در یک پروسه جدا است. مثلا IE8 رو دیدید که هر tab آن در یک پروسه جدا اجرا میشود، یا مثلا مرورگر کروم هم به همین صورت است. Application pool در IIS هم دقیقا همین معنا را دارد. عادت متداول، استفاده از یک Application pool به ازای تمام سایتها است که غلط است. یعنی زمانیکه این توانایی هست چرا استفاده نمیکنید؟ ثانیا در همین Application pool میتونید تنظیم کنید که اگر میزان مصرف حافظه برنامه منتسب، مثلا به 600 مگ رسید لطفا خودت به صورت خودکار اون رو recycle کن تا مشکلات کمبود حافظه رخ ندهد.
- از برنامههای dotTrace و Ants memory profiler هم میتونید استفاده کنید. اینها قابلیت دیباگ پروسههای مرتبط با IIS رو هم دارند و دقیقا گزارشات آماری پیشرفتهای میتونند از وضعیت مصرف حافظه قسمتهای مختلف برنامه در اختیار شما قرار بدن: (+) و (+)
یکی کردن اسمبلیهای یک پروژهی WPF
- یک try/catch در قسمت بارگذاری اسمبلی قرار دهید تا بهتر منبع مشکل را شناسایی کنید. یک مثال
- شخص دیگری در اینجا گزارش داده اگر Generate serialization assembly در قسمت تنظیمات پروژه، ذیل Build > Output فعال است، باید خاموش شود تا پروژه کرش نکند.
- اگر نوع اسمبلی، PCL است (Portable Class Library)، باز هم روش Assembly.Load به نحوی که در مطلب ذکر شده کار نمیکند و باید به صورت ذیل اصلاح شود:
private static Assembly loadEmbeddedAssembly(string name) { if (name.EndsWith("Retargetable=Yes")) { return Assembly.Load(new AssemblyName(name)); } // Rest of your code //... }
[MethodImpl(MethodImplOptions.NoOptimization)]