حالتهای عملکرد کتابخانهی Moq
کتابخانهی Moq، دو حالت عملکرد را دارد: Strict Mode و Loose mode. زمانیکه یک Mock object را نمونه سازی میکنیم، به صورت پیشفرض کتابخانهی Moq، یک Loose mock را ایجاد میکند. در این حالت این شیء، مقادیر پیشفرض خواص و اشیاء را بازگشت میدهد و استثنائی را صادر نمیکند. اگر این موارد مدنظر نیستند، میتوان به حالت Strict آن رجوع کرد که روش تنظیم آن به صورت زیر است:
var mockIdentityVerifier = new Mock<IIdentityVerifier>(MockBehavior.Strict);
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception: Moq.MockException: IIdentityVerifier.Initialize() invocation failed with mock behavior Strict. All invocations on the mock must have a corresponding setup.
mockIdentityVerifier.Setup(x => x.Initialize());
بنابراین هرچند کارکردن با حالت پیشفرض کتابخانهی Moq سادهاست، اما تنظیم حالت Strict سبب میشود تا تنظیمی را فراموش نکنیم و در نتیجه کیفیت آزمون واحد تهیه شده افزایش مییابد.
صدور استثناءها از طریق Mock objects
اگر در سیستم در حال آزمایش، قسمتی به بررسی خطاها اختصاص دارد، میتوان توسط Mock objects استثناءهایی را تولید و به این ترتیب منطق بررسی خطاها را آزمایش کرد.
برای نمونه در متد Process کلاس LoanApplicationProcessor، یک try/catch را به قسمت CalculateScore اضافه میکنیم:
try { _creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address); } catch { return application.IsAccepted; }
mockCreditScorer.Setup(x => x.CalculateScore(It.IsAny<string>(), It.IsAny<string>())) .Throws(new InvalidOperationException("Test Exception"));
Assert.IsFalse(application.IsAccepted);
صدور رخدادها از طریق Mock objects
فرض کنید یک EventArgs سفارشی را به صورت زیر تعریف:
using System; namespace Loans.Models { public class CreditScoreResultArgs : EventArgs { public int Score { get; set; } } }
public interface ICreditScorer { event EventHandler<CreditScoreResultArgs> ResultAvailable;
mockCreditScorer.Raise(x => x.ResultAvailable += null, new CreditScoreResultArgs());
روش دیگر انجام اینکار به صورت زیر است:
mockCreditScorer.Setup(x => x.CalculateScore(It.IsAny<string>(), It.IsAny<string>())) .Raises(x => x.ResultAvailable += null, new CreditScoreResultArgs());
معرفی Partial Mocks
در اغلب آزمونهای واحدی که تا اینجا بررسی شدند، ابتدا یک Mock object را ایجاد و سپس وهلهای از سرویس مدنظر را توسط آن تهیه میکنیم. در ادامه تعدادی از متدهای این سرویس را مانند متد Process کلاس LoanApplicationProcessor، فراخوانی میکنیم. اینکار سبب اجرای فعالیتی در این سیستم شده و به همراه آن تعاملی با اشیاء Mock شده نیز صورت میگیرد. در نهایت حالت و یا نتیجهای را دریافت میکنیم و آنرا با حالت یا نتیجهای که انتظار داریم، مقایسه خواهیم کرد. در این روش پس از پایان اجرای سیستم در حال اجرا، حالت و نتیجهی نهایی حاصل از عملکرد آن، مورد بررسی قرار میگیرد. این بررسیها را نیز بر روی اینترفیسها انجام دادیم. اگر بجای اینترفیسها از یک class استفاده شود، به آن partial mock گفته میشود. عموما مواردی را که آزمایش آنها سخت است، با Partial mocks پیاده سازی میکنند؛ مانند کار با فایل سیستم، کار با قطعه کدهای نامعین مانند DateTime.Now، اعداد اتفاقی و یا Guidها.
در مثال زیر، شبیه به متد آزمون واحد Accept که تاکنون آنرا بررسی کردیم، از اشیاء Mock شده استفاده شدهاست؛ با یک تفاوت: بجای اینترفیس IIdentityVerifier، از کلاس پیاده سازی کنندهی آن که در اینجا IdentityVerifierServiceGateway است، استفاده شده:
namespace Loans.Tests { [TestClass] public class LoanApplicationProcessorShould { [TestMethod] public void AcceptUsingPartialMock() { var product = new LoanProduct {Id = 99, ProductName = "Loan", InterestRate = 5.25m}; var amount = new LoanAmount {CurrencyCode = "Rial", Principal = 2_000_000_0}; var applicant = new Applicant {Id = 1, Name = "User 1", Age = 25, Address = "This place", Salary = 1_500_000_0}; var application = new LoanApplication {Id = 42, Product = product, Amount = amount, Applicant = applicant}; var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>(); mockIdentityVerifier.Setup(x => x.CallService(applicant.Name, applicant.Age, applicant.Address)) .Returns(true); var mockCreditScorer = new Mock<ICreditScorer>(); mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000); var sut = new LoanApplicationProcessor(mockIdentityVerifier.Object, mockCreditScorer.Object); sut.Process(application); Assert.IsTrue(application.IsAccepted); } } }
public virtual bool CallService(string applicantName, int applicantAge, string applicantAddress)
تبدیل DateTime.Now به یک مقدار ثابت قابل آزمایش توسط Partial Mocks
در کلاس IdentityVerifierServiceGateway، یک چنین کدی را داریم که از DateTime.Now نامشخص استفاده میکند و آزمون واحد نوشتن برای آن مشکل است؛ چون DateTime.Now در هربار که آزمایش اجرا میشود، تغییر میکند:
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = DateTime.Now; Disconnect(); return isValidIdentity; }
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = GetCurrentTime(); Disconnect(); return isValidIdentity; } public virtual DateTime GetCurrentTime() { return DateTime.Now; }
var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Setup(x => x.GetCurrentTime()) .Returns(expectedTime); // ... Assert.AreEqual(expectedTime, mockIdentityVerifier.Object.LastCheckTime);
استفاده از متدهای protected بجای استفاده از متدهای public virtual در Partial Mocks
همانطور که مشاهده کردید، برای کار با Partial Mocks نیاز است متدهای معرفی شده، از نوع public virtual باشند. برای نمونه حتی مجبور شدیم یک متد private را نیز public کنیم. اگر علاقمند به این نوع تغییرات نیستید، میتوان بجای public کردن متدهای private، آنها را protected تعریف کرد. به همین جهت دو متدی را که تاکنون public virtual تعریف کردیم، تبدیل به protected virtual میکنیم.
پس از آن در کلاسی که آزمونهای واحد را تهیه کردیم، ابتدا using Moq.Protected را ذکر میکنیم تا بتوانیم به قابلیتهای ویژهی کار با متدهای Protected دسترسی پیدا کنیم.
سپس روش تنظیم این نوع متدهای protected، چون دسترسی مستقیمی به آنها وجود ندارد، به صورت زیر، با ذکر نام رشتهای آنها تغییر میکند:
mockIdentityVerifier.Protected().Setup<bool>( "CallService",applicant.Name, applicant.Age, applicant.Address) .Returns(true); var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Protected().Setup<DateTime>("GetCurrentTime") .Returns(expectedTime);
روش دیگری نیز برای تعریف متدهای protected وجود دارد که اینبار strongly typed است. بالای متد آزمون واحد، اینترفیس private زیر را تعریف میکنیم:
interface IIdentityVerifierServiceGatewayProtectedMembers { DateTime GetCurrentTime(); bool CallService(string applicantName, int applicantAge, string applicantAddress); }
mockIdentityVerifier.Protected() .As<IIdentityVerifierServiceGatewayProtectedMembers>() .Setup(x => x.CallService(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<string>())) .Returns(true); var expectedTime = new DateTime(2000, 1, 1); mockIdentityVerifier.Protected() .As<IIdentityVerifierServiceGatewayProtectedMembers>() .Setup(x => x.GetCurrentTime()) .Returns(expectedTime);
معرفی روش دیگری بجای استفاده از متدهای protected
اگر در کدهای خود نیاز به استفادهی بیش از حد از متدهای protected را مشاهده کردید، این مورد میتوان نشانهی امکان Refactoring این قسمت از کدها به سرویسهایی مجزا باشند. برای مثال میتوان یک اینترفیس INowProvider را به صورت زیر تعریف کرد:
using System; namespace Loans.Services.Contracts { public interface INowProvider { DateTime GetNow(); } }
public class IdentityVerifierServiceGateway : IIdentityVerifier { private readonly INowProvider _nowProvider; public DateTime LastCheckTime { get; private set; } public IdentityVerifierServiceGateway(INowProvider nowProvider) { _nowProvider = nowProvider; }
public bool Validate(string applicantName, int applicantAge, string applicantAddress) { Connect(); var isValidIdentity = CallService(applicantName, applicantAge, applicantAddress); LastCheckTime = _nowProvider.GetNow(); // ...
var mockNowProvider = new Mock<INowProvider>(); mockNowProvider.Setup(x => x.GetNow()).Returns(expectedTime); var mockIdentityVerifier = new Mock<IdentityVerifierServiceGateway>(mockNowProvider.Object);
به مثال زیر توجه کنید:
class EventSource : System.Progress<int> { public async System.Threading.Tasks.Task<int> PerformExpensiveCalculation() { var sum = 0; for (var i = 0; i < 100; i++) { await System.Threading.Tasks.Task.Delay(100); sum += i; this.OnReport(sum); } return sum; } } static class Program { static void Main(string[] args) { var source = new EventSource(); System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress); source.ProgressChanged += handler; System.Console.WriteLine(source.PerformExpensiveCalculation().Result); source.ProgressChanged -= handler; source.ProgressChanged += ProgressChangedMethod; System.Console.WriteLine(source.PerformExpensiveCalculation().Result); source.ProgressChanged -= ProgressChangedMethod; } private static void ProgressChangedMethod( object sender, int e ) { System.Console.WriteLine(e); } }
خوب؛ برای اندازه گیری کارآیی این دو روش باید کمی فکر کنیم که چه چیزی کارآیی این دو روش را تغییر میدهد؟
آیا پردازش event با اضافه کردن و حذف کردن event handler؟ و یا پردازش درون event باعث تغییر در کارآیی میشود؟
این، سوال مهمی در تست کارآیی این دو روش مختلف است. اگر پردازش درون event باعث ایجاد تفاوت کارآیی میشود، با استفاده از این برنامه میتوان آن را اندازه گیری کرد. با این حال اگر تفاوت کارآیی با اضافه کردن و حذف کردن event handler اتفاق میافتد، با این برنامه بعید است بتوان این روش را تست کرد چرا که فقط یکبار این عمل انجام میشود.
قبل از شروع به اندازه گیری کارآیی این دو روش، اجازه بدهید ابتدا به کد IL آنها نگاهی کنیم. (روش اول با استفاده از Lambda syntax)
IL_0007: ldsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__0_0' IL_000c: dup IL_000d: brtrue.s IL_0026 IL_000f: pop IL_0010: ldsfld class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9' IL_0015: ldftn instance void LambdaPerformance.Program/'<>c'::'<Main>b__0_0'(object, int32) IL_001b: newobj instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int) IL_0020: dup IL_0021: stsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__0_0' IL_0026: stloc.1 IL_0027: ldloc.0 IL_0028: ldloc.1 IL_0029: callvirt instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>) IL_002e: nop IL_002f: ldloc.0 IL_0030: callvirt instance class [mscorlib]System.Threading.Tasks.Task`1<int32> LambdaPerformance.EventSource::PerformExpensiveCalculation() IL_0035: callvirt instance !0 class [mscorlib]System.Threading.Tasks.Task`1<int32>::get_Result() IL_003a: call void [mscorlib]System.Console::WriteLine(int32) IL_003f: nop IL_0040: ldloc.0 IL_0041: ldloc.1 IL_0042: callvirt instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
قبل از شروع مقایسه، کد IL روش دوم را نیز بررسی میکنیم:
IL_004a: ldftn void LambdaPerformance.Program::ProgressChangedMethod(object, int32) IL_0050: newobj instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int) IL_0055: callvirt instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>) IL_005a: nop IL_005b: ldloc.0 IL_005c: callvirt instance class [mscorlib]System.Threading.Tasks.Task`1<int32> LambdaPerformance.EventSource::PerformExpensiveCalculation() IL_0061: callvirt instance !0 class [mscorlib]System.Threading.Tasks.Task`1<int32>::get_Result() IL_0066: call void [mscorlib]System.Console::WriteLine(int32) IL_006b: nop IL_006c: ldloc.0 IL_006d: ldnull IL_006e: ldftn void LambdaPerformance.Program::ProgressChangedMethod(object, int32) IL_0074: newobj instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int) IL_0079: callvirt instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>)
برای اندازه گیری دقیق، برنامهی بالا را کمی تغییر میدهیم. ما میزان اضافه و حذف شدن event handler را میخواهیم اندازهگیری کنیم و کاری به زمان اجرای یک عملیات نداریم. بنابراین فراخوانی ()PerformExpensiveCalculation را comment کرده و به صورت خیلی ساده فقط handler را اضافه و حذف میکنیم.
static class Program { static void Main(string[] args) { for (var repeats = 10; repeats <= 1000000; repeats *= 10) { VersionOne(repeats); VersionTwo(repeats); } } private static void VersionOne(int repeats) { var timer = new System.Diagnostics.Stopwatch(); timer.Start(); var source = new EventSource(); for (var i = 0; i < repeats; i++) { System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress); source.ProgressChanged += handler; // Console.WriteLine(source.PerformExpensiveCalculation().Result); source.ProgressChanged -= handler; } timer.Stop(); System.Console.WriteLine($"Version one: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms"); } private static void VersionTwo(int repeats) { var timer = new System.Diagnostics.Stopwatch(); timer.Start(); var source = new EventSource(); for (var i = 0; i < repeats; i++) { source.ProgressChanged += ProgressChangedMethod; // Console.WriteLine(source.PerformExpensiveCalculation().Result); source.ProgressChanged -= ProgressChangedMethod; } timer.Stop(); System.Console.WriteLine($"Version two: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms"); } private static void ProgressChangedMethod(object sender, int e) { System.Console.WriteLine(e); } }
Version one: 10 add/remove takes 0ms Version two: 10 add/remove takes 0ms Version one: 100 add/remove takes 0ms Version two: 100 add/remove takes 0ms Version one: 1000 add/remove takes 0ms Version two: 1000 add/remove takes 0ms Version one: 10000 add/remove takes 0ms Version two: 10000 add/remove takes 1ms Version one: 100000 add/remove takes 8ms Version two: 100000 add/remove takes 13ms Version one: 1000000 add/remove takes 93ms Version two: 1000000 add/remove takes 121ms
توجه: اگر در برنامهی شما یک میلیون بار event handler اضافه و حذف میشوند، نیاز به بازنگری مجدد در طراحی کلی برنامه تان دارد.
یک اشتباه بزرگ
با ایجاد یک تغییر در روش اول (Lambda syntax)، ممکن است تاثیر بسیار زیادی را در عملکرد برنامه مشاهده کنید:private static void VersionOne(int repeats) { var timer = new System.Diagnostics.Stopwatch(); timer.Start(); var source = new EventSource(); for (var i = 0; i < repeats; i++) { // System.EventHandler<int> handler = (_, progress) => System.Console.WriteLine(progress); source.ProgressChanged += (_, progress) => System.Console.WriteLine(progress); // Console.WriteLine(source.PerformExpensiveCalculation().Result); source.ProgressChanged -= (_, progress) => System.Console.WriteLine(progress); } timer.Stop(); System.Console.WriteLine($"Version one: {repeats} add/remove takes {timer.ElapsedMilliseconds}ms"); }
Version one: 10 add/remove takes 0ms Version two: 10 add/remove takes 0ms Version one: 100 add/remove takes 1ms Version two: 100 add/remove takes 0ms Version one: 1000 add/remove takes 102ms Version two: 1000 add/remove takes 0ms Version one: 10000 add/remove takes 10509ms Version two: 10000 add/remove takes 1ms Version one: 100000 add/remove takes 1039014ms Version two: 100000 add/remove takes 11ms
IL_0018: nop IL_0019: ldloc.1 IL_001a: ldsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_0' IL_001f: dup IL_0020: brtrue.s IL_0039 IL_0022: pop IL_0023: ldsfld class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9' IL_0028: ldftn instance void LambdaPerformance.Program/'<>c'::'<VersionOne>b__1_0'(object, int32) IL_002e: newobj instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int) IL_0033: dup IL_0034: stsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_0' IL_0039: callvirt instance void class [mscorlib]System.Progress`1<int32>::add_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>) IL_003e: nop IL_003f: ldloc.1 IL_0040: ldsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_1' IL_0045: dup IL_0046: brtrue.s IL_005f IL_0048: pop IL_0049: ldsfld class LambdaPerformance.Program/'<>c' LambdaPerformance.Program/'<>c'::'<>9' IL_004e: ldftn instance void LambdaPerformance.Program/'<>c'::'<VersionOne>b__1_1'(object, int32) IL_0054: newobj instance void class [mscorlib]System.EventHandler`1<int32>::.ctor(object, native int) IL_0059: dup IL_005a: stsfld class [mscorlib]System.EventHandler`1<int32> LambdaPerformance.Program/'<>c'::'<>9__1_1' IL_005f: callvirt instance void class [mscorlib]System.Progress`1<int32>::remove_ProgressChanged(class [mscorlib]System.EventHandler`1<!0>) IL_0064: nop IL_0065: nop IL_0066: ldloc.2 IL_0067: stloc.3 IL_0068: ldloc.3 IL_0069: ldc.i4.1 IL_006a: add IL_006b: stloc.2 IL_006c: ldloc.2 IL_006d: ldarg.0 IL_006e: clt IL_0070: stloc.s V_4 IL_0072: ldloc.s V_4 IL_0074: brtrue.s IL_0018
ممکن است شما به این نتیجه رسیده باشید که استفاده از Lambda syntax برای اضافه و حذف کردن event handler آهستهتر از، استفاده از متد جدا است، این یک اشتباه بزرگ است. در صورتی که شما اضافه و حذف کردن event handler را با استفاده از Lambda syntax به شکل صحیح انجام ندهید، به سرعت، در معیارهای کارآیی خود را نشان میدهد.
دانلود برنامه بالا
متدهای الحاقی و ترکیب کنندههای اعمال غیرهمزمان
متد WhenAll
کار آن ترکیب تعدادی Task است و اجرای آنها. تنها زمانی خاتمه مییابد که کلیهی Taskهای معرفی شده به آن خاتمه یافته باشند. هدف از آن اجرای همزمان و مستقل چندین Task است. برای مثال دریافت چندین فایل به صورت همزمان از اینترنت.
همچنین باید دقت داشت که در اینجا، هر Task کاری به نتایج Taskهای دیگر ندارد و کاملا مستقل اجرا میشود. اگر نیاز است Taskها مستقل اجرا شوند، از همان روش سریالی اجرای Taskها، توسط معرفی هر کدام به کمک await استفاده کنید.
به علاوه اگر در این بین استثنایی وجود داشته باشد، تنها پس از پایان عملیات تمام Taskها بازگشت داده میشود. این استثناء نیز از نوع Aggregate Exception است.
using System.Linq; using System.Threading.Tasks; namespace Async07 { public class EggBoiler { private const int BoilingTimeMs = 200; private static Task boilEgg() { var bolingTask = Task.Run(() => { Task.Delay(BoilingTimeMs); }); return bolingTask; } public async Task BoilEggsSequentialAsync(int count) { for (var i = 0; i < count; i++) { await boilEgg(); } } public async Task BoilEggsSimultaneousAsync(int count) { var tasksList = from egg in new[] { 1, 2, 3, 4, 5 } select boilEgg(); await Task.WhenAll(tasksList); // ... } } }
اما در متد BoilEggsSimultaneousAsync به علت بکارگیری Task.WhenAll پختن تمام تخم مرغهای مدنظر همزمان آغاز میشود و تا پایان عملیات (پخته شدن تمام تخم مرغها) صبر خواهد شد.
متد WhenAny
در حالت استفاده از متد WhenAny، هر کدام از Taskهای در حال پردازش که خاتمه یابند، کل عملیات خاتمه خواهد یافت. فرض کنید نیاز دارید تا دمای کنونی هوای منطقهی خاصی را از چند وب سرویس مختلف دریافت کنید. میتوان در این حالت تمام اینها را توسط WhenAny ترکیب کرد و هر کدام که زودتر خاتمه یابد، عملیات را پایان خواهد داد.
public class Downloader { private Task<string> downloadTask(string url) { return new WebClient().DownloadStringTaskAsync(url); } public async Task<int> GetTemperature() { var sites = new[] { "http://www.site1.com/svc", "http://www.site2.com/svc", "http://www.site3.com/svc", }; var tasksList = from site in sites select downloadTask(site); try { var finishedTask = await Task.WhenAny(tasksList); var result = await finishedTask; } catch (Exception ex) { } // todo: process result, get temperature return 10; // for example. } }
در این حالت اگر نیاز بود وضعیت سایر Taskها، مثلا در صورت شکست آنها، بررسی شوند، میتوان از یکی از دو قطعه کد زیر استفاده کرد:
foreach (var task in tasksList) { var ignored = task.ContinueWith( t => Console.WriteLine(t.Exception), TaskContinuationOptions.OnlyOnFaulted); } // or foreach (var task in tasksList) { var ignored = task.ContinueWith( t => { if (t.IsFaulted) Console.WriteLine(t.Exception); }); }
کاربرد دیگر WhenAny زمانی است که برای مثال میخواهید تعداد زیادی Url را پردازش کنید، اما نمیخواهید برای نمایش اطلاعات، تا پایان عملیات تمامی آنها مانند WhenAll صبر کنید. میخواهید به محض پایان کار یکی از Taskها، عملیات نمایش نتیجهی آنرا انجام دهید:
public async Task ShowTemperatures() { var sites = new[] { "http://www.site1.com/svc", "http://www.site2.com/svc", "http://www.site3.com/svc", }; var tasksList = sites.Select(site => downloadTask(site)).ToList(); while (tasksList.Any()) { try { var tempTask = await Task.WhenAny(tasksList); tasksList.Remove(tempTask); var result = await tempTask; //todo: show result } catch(Exception ex) { } } }
کاربرد سوم WhenAny کنترل تعداد وظایف همزمان است. برای مثال اگر قرار است هزاران تصویر از اینترنت دریافت شوند، نباید تمام وظایف را یکجا راه اندازی کرد. شاید نیاز باشد هربار فقط 15 وظیفهی همزمان عمل کنند و نه بیشتر. در این حالت، مثال قبلی دارای یک حلقهی کنترل کننده tasksList ارائه شده خواهد شد. هر بار تعداد معینی وظیفه به tasksList اضافه و پردازش میشوند و این روند تا پایان کار تعداد Urlها ادامه خواهد یافت (یک Take و Skip است؛ مانند صفحه بندی اطلاعات).
متدهای Run و FromResult
متد Task.Run اضافه شده در دات نت 4.5 به این معنا است که میخواهید Task ایجاد شده بر روی Thread pool اجرا شود. پارامتر آن میتواند یک delegate یا عبارت lambda و یا حتی یک Task باشد. خروجی آن نیز یک Task است و به همین جهت با async و await سی شارپ 5 سازگاری بهتری دارد.
استفاده از Task.Run نسبت به عملیات Threading متداول کارآیی بهتری دارد، زیرا ایجاد Threadهای جدید زمانبر بوده و زمانیکه به صورت خودکار از Thread pool استفاده میشود، تا حد امکان، استفادهی مجدد از تردهای بیکار در حال حاضر، مدنظر است.
متد Task.FromResult کار بازگشت یک Task را از نتایج متدهای مختلف فراهم میکند. فرض کنید یک متد async تعریف کردهاید که خروجی آن Task of T است. در اینجا اگر داخل متد، از یک متد معمولی که یک عدد int را ارائه میدهد استفاده کنیم، با استفاده از Task.FromResult بلافاصله میتوان یک Task of int را بازگشت داد.
متد Delay
پیشتر برای به خواب فرو بردن یک ترد از متد Thread.Sleep استفاده میشد. کار Thread.Sleep بلاک کردن ترد جاری است. در دات نت 4.5، بجای آن باید از Task.Delay استفاده شود که یک مکانیزم غیر قفل کننده را جهت صبر کردن به همراه بازگشت یک Task، ارائه میدهد.
یکی از کاربردهای Delay منهای صبر کردن تا مدت زمانی مشخص، ایجاد مکانیزم timeout است. برای مثال حالت Task.WhenAny را درنظر بگیرید. اگر در اینجا timeout مدنظر ما 3 ثانیه باشد، میتوان یکی از Taskها را Task.Delay با آرگومان مساوی 3000 معرفی کرد. اگر هر کدام از taskهای تعریف شده زودتر از 3 ثانیه پایان یافتند که بسیار خوب؛ در غیر اینصورت Task.Delay معرفی شده کار را تمام میکند.
متد Yield
متد Task.Yield بسیار شبیه به متد قدیمی DoEvents است که از آن برای اجازه دادن به سایر اعمال جهت اجرا، در بین یک عمل طولانی، استفاده میشد.
متد ConfigureAwait
به صورت پیش فرض ادامه یک عملیات همزمان، بر روی ترد ایجاد کنندهی آن اجرا میشود. برای نمونه اگر یک عملیات async در ترد UI آغاز شود، نتیجهی آن نیز در همان ترد UI بازگشت داده میشود. به این ترتیب دیگر نیازی نخواهد بود تا نگرانی در مورد نحوهی دسترسی به مقدار آن توسط عناصر UI داشته باشیم.
اگر به این مساله اهمیت نمیدهید، برای مثال اگر اعمال در حال انجام، کاری به عناصر UI ندارند، از متد ConfigureAwait با پارامتر false بر روی یک task پیش از فراخوانی await بر روی آن، استفاده کنید.
byte [] buffer = new byte[0x1000]; int numRead; while((numRead = await source.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false)) > 0) { await source.WriteAsync(buffer, 0, numRead).ConfigureAwait(false); }
به صورت خلاصه در سی شارپ 5
- بجای task.Wait قدیمی، از await task برای صبر کردن تا پایان یک task استفاده کنید.
- بجای task.Result جهت دریافت یک نتیجهی یک task از await task کمک بگیرید.
- بجای Task.WaitAll از await Task.WhenAll و بجای Task.WaitAny از await Task.WhenAny استفاده نمائید.
- همچنین Thread.Sleep در اعمال async با await Task.Delay جایگزین شدهاست.
- در اعمال غیرهمزمان همیشه متد ConfigureAwait false را بکار بگیرید، مگر اینکه به Context نهایی آن واقعا نیاز داشته باشید.
و برای ایجاد یک Task جدید از Task.Run یا TaskFactory.StartNew استفاده نمائید.
ویژگیهای مورد بحث شامل موارد زیر میباشند:
• ویرایش TextBoxها در محیط Designer
• Copy و Paste کردن Styleها از یک کنترل به کنترل دیگر
• قالب بندی شرطی
• پیمایش و تغییر اندازه گزارش و آیتمهای آن
• تغییر اندازه بخشهای مختلف گزارش نظیر Page Header ، Detail و ...
• افزودن TextBox Shape و PictureBox درون Designer گزارش
• استفاده از پنجره Data Explorer
• استفاده از پنجره Report Explorer
در ادامه به بررسی موارد ذکر شده جهت طراحی گزارش میپردازیم.
1. جهت تغییر محتوای یک TextBox میتوان روی آن دوبار کلیک نمود. پس از آن TextBox به حالت ویرایش میرود و میتوان متن درون آن را به سادگی تغییر داد.این کار در محیط Designer انجام میشود و نیازی نیست برای تغییر محتوای TextBox به پنجره Properties بروید و متن آن را تغییر دهید.
2. در محیط Designer به راحتی میتوانید گزارش خود را Zoom نمایید.این کار توسط ComboBox مربوطه در پایین سمت چپ Designer انجام میشود.
3. اگر قرار باشد TextBox جدیدی به گزارش خود اضافه نمایید، کافی است آن را از بخش ToolBox به محیط گزارش بکشید و سپس به چینش آن بپردازید.
4. در محیط طراح گزارش Telerik میتوانید به راحتی یک قالب بندی و یا Style را از کنترلی به کنترل دیگر کپی نمایید و در وقت خود جهت طراحی گزارش صرفه جویی نمایید. برای انجام اینکار کافی است روی کنترلی که قرار است Style آن را کپی نمایید راست کلیک نموده و پس از آن از منوی ظاهر شده گزینه Copy Style را انتخاب نمایید. در ادامه میتوانید کنترل و یا کنترل هایی که قرار است قالب بندی را به آنها اعمال کنید انتخاب نموده ، روی آنها راست کلیک نمایید و گزینه Paste Style را انتخاب کنید.با این کار Style ی که در مرحله قبل از کنترلی دیگر کپی کرده بودید به کنترل یا کنترلهای انتخاب شده اعمال میشود.
5. یکی دیگر از امکانات Telerik Reporting امکان قالب بندی شرطی (Conditional Formating) میباشد. یعنی Style یک کنترل توسط شرطها تعیین میشود. برای مثال میتوانیم بگوییم که اگر مقدار فروش بیشتر از مبلغ خاصی بود ، عدد نمایش داده شده با رنگ سبز نمایش داده شود و یا اینکه از فونت و یا اندازه دیگری جهت نمایش آن استفاده شود (به طور کلی با توجه به شرطهای تعیین شده نمایش آن کنترل با یک Style متفاوت صورت گیرد). در قسمتهای آینده به بررسی کامل این قابلیت نیز خواهیم پرداخت.
6. یکی از امکاناتی که در هنگام طراحی گزارش در اختیار ما قرار میگیرد پنجره Data Explorer میباشد. توسط این پنجره میتوان فیلدهای یک منبع داده (DataSource) را مشاهده نمود و برای اینکه بتوان از آنها در محیط طراحی استفاده کرد بر روی محیط طراحی درگ نمود. DataSourceها انواع مختلفی دارند که در قسمت اول این آموزش به معرفی آنها پرداختیم و از نمونه Sql آن نیز جهت طراحی یک گزارش ساده استفاده کردیم. در ادامه نیز با این موارد بیشتر آشنا خواهید شد.در تصویر زیر نحوهی درگ کردن یک فیلد تصویر را از پنجره Data Explorer مشاهده مینمایید.
7.
یکی دیگر از اجزای Reporting پنجرهی Report Explorer میباشد. توسط این
پنجره میتوان دسترسی سریعی به اجزای درون گزارش داشت. برای مثال میتوان
به راحتی یک بخش درون گزارش را انتخاب نمود و در پنجرهی Properties
تغییراتی در آن اعمال نمود.
ادامه دارد ...
SET STATISTICS TIME ON
SELECT ProductID, StartDate, EndDate, StandardCost FROM Production.ProductCostHistory WHERE StandardCost < 500.00;
SQL Server parse and compile time: CPU time = 0 ms, elapsed time = 1 ms. (269 row(s) affected) SQL Server Execution Times: CPU time = 1 ms, elapsed time = 2 ms.
-
زمان کامپایل و تجزیه و تحلیل ( parse and compile time) : زمانیکه یک کوئری را برای اجرا به SQL Server ارائه میدهید، SQL Server آنرا از نظر خطای نحوی بررسی مینماید و بهینه ساز یک نقشه بهینه را برای اجرا تولید مینماید. اگر به خروجی نگاه کنید، زمان پردازش ( CPU time) و زمان سپری شده ( elapsed time) را نشان می دهد. منظور از زمان پردازش زمان واقعی صرف شده روی پردازنده میباشد و زمان سپری شده اشاره به زمان تکمیل شدن عملیات کامپایل و تجزیه و تحلیل میباشد. تفاوت بین زمان پردازش و زمان سپری شده ممکن است زمان انتظار در صف برای گرفتن پردازنده و یا انتظار برای تکمیل عملیات IO باشد.
-
زمان اجرا ( Execution Times) : این زمان اشاره به زمان سپری شده برای تکمیل اجرای نقشه کامپایل شده دارد. زمان پردازش اشاره به زمان واقعی صرف شده روی پردازنده دارد و زمان سپری شده نیز مجموع زمان صرف شده تا تکمیل اجرای دستور که شامل زمان انتظار برای تکمیل عملیات IO و زمان صرف شده برای انتقال خروجی به کلاینت میباشد، دارد. زمان پردازش میتواند به عنوان مبنایی برای اندازهگیری کارآیی مورد استفاده قرار بگیرد. این مقدار در اجراهای مختلف تفاوت چندانی با هم ندارند جز اینکه کوئری و یا دادهها تغییر نمایند. توجه نمایید که زمان براساس میلی ثانیه میباشد. زمان سپری شده نیز به فاکتورهایی مانند بارگذاری روی سرور، بارگذاری IO، و پهنای باند بین سرور و کلاینت وابسته میباشد. بنابراین همیشه زمان پردازش به عنوان مبنایی برای اندازهگیری کارایی استفاده میشود .
ASP.NET MVC #20
تهیه گزارشات تحت وب به کمک WebGrid
WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس میباشد و هدف از آن سادهتر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونههای دیگری را ارائه دادهاند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانههای جانبی در دسترس است، بررسی آن ضروری میباشد.
صورت مساله
لیستی از کارمندان به همراه حقوق ماهیانه آنها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر میباشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینکهای حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخهای موجود در لیست، میلادی هستند. نیاز است این تاریخها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری میباشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژههای متناظر را توسط یک گرید تو در تو نمایش دهید.
راه حل به کمک استفاده از WebGrid
ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدلهای زیر را به آن اضافه نمائید (یک کارمند که میتواند تعداد پروژه منتسب داشته باشد):
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}
namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}
سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید، تکرار کنید.
using System;
using System.Collections.Generic;
namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}
private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}
در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;
namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}
[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}
[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}
علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر میگردد:
http://localhost:3034/?sort=LastName&sortdir=ASC&page=3
همانطور که ملاحظه میکنید، گرید رندر شده، از یک سری کوئری استرینگ برای مشخص سازی صفحه جاری، یا جهت مرتب سازی (صعودی و نزولی بودن آن) یا فیلد پیش فرض مرتب سازی، کمک میگیرد.
سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه میکنید:
@using System.Globalization
@model IList<MvcApplication17.Models.Employee>
@{
ViewBag.Title = "Index";
}
@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}
@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}
<h2>Employees List</h2>
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
توضیحات ریز جزئیات View فوق
تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا میتوان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستونها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده میشود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:
@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}
در اینجا همچنین سه متغیر کمکی هم تعریف شده که از اینها برای تهیه جمع ستونهای حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده میشود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه میکنید. شماره ردیفهای بعدی، rowIndex++ خواهند بود.
تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشتهای معادل اطلاعات HTML صفحه جاری، بازگشت داده میشود. در اینجا میتوان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
هر کدام از این رشتهها در حین رندر نهایی گرید، تبدیل به یک class خواهند شد. برای نمونه:
<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">
به این ترتیب با اندکی ویرایش css سایت، میتوان انواع و اقسام رنگها را به سطرها و ستونهای گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:
/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}
.webgrid a
{
color: #000;
}
.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}
.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}
.webgrid-selected-row
{
font-weight: bold;
}
.text-align-center-col
{
text-align: center;
}
.total-row
{
background-color:#f9eef4;
}
همانطور که ملاحظه میکنید، رنگهای ردیفها، هدر و فوتر گرید و غیره در اینجا تنظیم میشوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery استفاده خواهیم کرد.
تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All میباشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن میتوان نوع Pager گرید را تعیین کرد:
[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}
نحوه تعریف ستونهای گرید
اکنون به مهمترین قسمت تهیه گزارش رسیدهایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستونهای مختلف مشخص میگردد. مقداری که باید در اینجا تنظیم شود، آرایهای از نوع WebGridColumn میباشد و مرسوم است به کمک متد کمکی grid.Columns، اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر میگرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن، نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص میشود. به کمک پارامتر header، عبارت سرستون متناظر تنظیم میگردد. پارامتر format، مهمترین و توانمندترین پارامتر متد grid.Column است:
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
پارامتر format، به نحو زیر تعریف شده است:
Func<dynamic, object> format
به این معنا که هر بار پیش از رندر سطر جاری، زمانیکه قرار است سلولی رندر شود، یک شیء dynamic در اختیار شما قرار میگیرد. این شیء dynamic یک رکورد از اطلاعات Model جاری است. به این ترتیب به اطلاعات تمام سلولهای ردیف جاری دسترسی خواهیم داشت. بر این اساس هر نوع پردازشی را که لازم بود، انجام دهید (شبیه به فرمول نویسی در ابزارهای گزارش سازی، اما اینبار با کدهای سی شارپ) و مقدار فرمت شده نهایی را به صورت یک رشته بر گردانید. این رشته نهایتا در سلول جاری درج خواهد شد.
اگر از پارامتر فرمت استفاده نشود، همان مقدار فیلد جاری بدون تغییری رندر میگردد.
حداقل به دو نحو میتوان پارامتر فرمت را مقدار دهی کرد:
format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}
مستقیما از توانمندیهای Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهلهای از رکورد در دسترس اشاره میکند که در اینجا وهلهای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده میکنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.
نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.
نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین میتوان آنرا با مقدار دهی صحیح پارامتر format ایجاد کرد:
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره میکند.
نحوه تبدیل تاریخها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت میکنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت میدهیم:
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),
اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن میتوانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا میتوان نحوه بازگشت مقدار حقوق را به صورت رشتهای حاوی جدا کنندههای سه رقمی نیز مشاهده کرد:
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
اضافه کردن گردیدهای تو در تو
متد Grid.GetHtml، یک رشته را بر میگرداند. بنابراین در هر چند سطح که نیاز باشد میتوان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
در اینجا کار اصلی از طریق پارامتر format شروع میشود. سپس به کمک item.Projects به لیست پروژههای هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت میدهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کردهایم.
اضافه کردن دکمههای ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستونهایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت میکند:
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink میباشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow میباشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحهای ارسال شود، میتوان از روش زیر استفاده کرد:
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
نمایش برچسبهای صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازهای از تعداد رکوردهای کلی را تشکیل میدهند. برای این منظور چند متد کمکی به نامهای WebGridPageFirstItem و WebGridPageLastItem تهیه شدهاند که آنها را در ابتدای View ارائه شده، مشاهده نمودید:
<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount
نمایش جمع ستونهای حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمیدهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:
@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}
در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه میشود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:
<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>
چگونه میتوانم یک پلاک خودرو را در سی شارپ بخوانم
- اگر علاقمند به نوشتن یک OCR هستید، این مطلب و نظرات آنرا مطالعه کنید. حداقل یک دید کلی نسبت به روش کار آن و هوش مصنوعی بکار گرفته شدهی در OpenCV پیدا میکنید.
- همچنین این سری پردازش تصویر با پایتون هم مفید است که به همراه دو ویدیوی OCR هم هست: ^ و ^. با توجه به اینکه پایتون نیز در پشت صحنه از همین OpenCV استفاده میکند، پس از آشنایی با روش کار، امکان ترجمهی کدهای آن به #C، یا هر زبان دیگری هم وجود دارد (پایتون در اینجا فقط یک اینترفیس است و کار اصلی را OpenCV انجام میدهد).