/// <summary> /// It returns true if string is null or empty or just a white space otherwise it returns false. /// </summary> /// <param name="input">Input String</param> /// <returns>bool</returns> public static bool IsEmpty(this string input) { return string.IsNullOrEmpty(input) || string.IsNullOrWhiteSpace(input); }
در گزارشات Crosstab، ردیفهای یک گزارش، تبدیل به ستونهای آن میشوند؛ به همین جهت به آنها Pivot tables هم میگویند.
برای مثال فرض کنید که قصد دارید گزارش تعداد ساعت کارکرد را به ازای هر پروژه در طول چند ماه تعیین کنید. گزارش متداول از این نوع اطلاعات، یک لیست بلند بالای بیمفهوم است. این گزارش تشکیل شده از صدها رکورد به ازای کارکنان مختلف در پروژههای مختلف و ... هیچ ارزش آماری خاصی ندارد. یک گزارش بدوی است. زمانیکه این گزارش را تبدیل به حالت crosstab میکنیم، اولین ستون فقط یک شماره پروژه خواهد بود و ستونهای بعدی، مثلا نام ماهها و مقادیر آنها هم جمع کارکرد افراد بر روی یک پروژه مشخص.
مثال اول) تهیه گزارش Crosstab جمع هزینههای واحدهای مختلف به تفکیک ماه
کلاس هزینههای زیر را در نظر بگیرید که به کمک آن میتوان به ازای هر واحد یا دپارتمان در تاریخهای متفاوت، هزینهای را مشخص ساخت:
using System;
namespace Pivot.Sample1
{
public class Expense
{
public DateTime Date { set; get; }
public string Department { set; get; }
public decimal Expenses { set; get; }
}
}
با توجه به این کلاس، یک منبع داده آزمایشی جهت تهیه گزارشات، میتواند به صورت زیر باشد:
using System;
using System.Collections.Generic;
namespace Pivot.Sample1
{
public class ExpenseDataSource
{
public static IList<Expense> ExpensesDataSource()
{
return new List<Expense>
{
new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },
new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
};
}
}
}
و اگر این لیست را به همین شکلی که هست نمایش دهیم، خروجی زیر را خواهیم داشت:
که ... خروجی مطلوبی نیست. در اینجا ما فقط 9 رکورد داریم؛ اما در عمل به ازای هر روز، یک رکورد میتواند وجود داشته باشد و این لیست طولانی، هیچ ارزش آماری خاصی ندارد. میخواهیم سرستونهای گزارش ما مطابق جدول زیر باشند:
یعنی اگر سه ماه را در نظر بگیریم با هر تعداد رکورد، فقط سه ردیف به ازای هر ماه باید حاصل شود و ستونهای دیگر هم نام بخشها یا واحدهای موجود باشند.
برای رسیدن به این خروجی Crosstab، میتوان کوئری LINQ زیر را به کمک امکانات گروه بندی اطلاعات آن تهیه کرد:
using System.Collections;
using System.Linq;
namespace Pivot.Sample1
{
public class PivotTable
{
public static IList ExpensesCrossTab()
{
return ExpenseDataSource
.ExpensesDataSource()
.GroupBy(t =>
new
{
Year = t.Date.Year,
Month = t.Date.Month
})
.Select(myGroup =>
new
{
//Year = myGroup.Key.Year,
Month = myGroup.Key.Month,
ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
})
.ToList();
}
}
}
که اینبار خروجی زیر را تولید میکند.
اگر علاقمند باشید که مثال فوق را در برنامهی LINQPad آزمایش کنید، این فایل را دریافت نموده و در آن برنامه باز نمائید.
مثال دوم) تهیه لیست Crosstab حضور و غیاب افراد در طول یک هفته
کلاس StudentStat را جهت ثبت اطلاعات حضور یک دانشجو، میتوان به شکل زیر تعریف کرد:
using System;
namespace Pivot.Sample2
{
public class StudentStat
{
public int Id { set; get; }
public string Name { set; get; }
public DateTime Date { set; get; }
public bool IsPresent { set; get; }
}
}
و بر همین اساس یک منبع داده فرضی جهت انجام گزارشات میتواند به نحو زیر تهیه شود:
using System;
using System.Collections.Generic;
namespace Pivot.Sample2
{
public class StudentsStatDataSource
{
public static IList<StudentStat> CreateMonthlyReportDataSource()
{
var result = new List<StudentStat>();
var rnd = new Random();
for (int day = 1; day < 6; day++)
{
for (int student = 1; student < 6; student++)
{
result.Add(new StudentStat
{
Id = student,
Date = new DateTime(2011, 11, day),
IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
Name = "student " + student
});
}
}
return result;
}
}
}
خروجی این گزارش هم در این حالت ساده با 5 دانشجو و فقط 5 روز، 25 رکورد خواهد بود:
که ... این هم آنچنان از لحاظ آماری مطلوب و مفهوم نیست. میخواهیم سطرهای این گزارش همانند لیست واقعی حضورغیاب، فقط از نام افراد تشکیل شود و همچنین ستونها مثلا شماره یا نام روزهای یک هفته یا ماه باشند. مثلا به شکل زیر:
برای رسیدن به این خروجی Crosstab، مثلا میتوان از کوئری LINQ زیر کمک گرفت که بر اساس شماره دانشجویی اطلاعات را گروه بندی کرده است:
using System.Collections;
using System.Linq;
namespace Pivot.Sample2
{
public class PivotTable
{
public static IList StudentsStatCrossTab()
{
return StudentsStatDataSource
.CreateWeeklyReportDataSource()
.GroupBy(x =>
new
{
x.Id
})
.Select(myGroup =>
new
{
myGroup.Key.Id,
Name = myGroup.First().Name,
Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
})
.ToList();
}
}
}
و این کوئری خروجی زیر را تولید میکند که از هر لحاظ نسبت به لیست قبلی مفهومتر است:
فایل LINQPad این مثال را میتوانید از اینجا دریافت کنید.
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{id:int}")] public IActionResult Get(int id) { return Ok(id); } }
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{id}")] public IActionResult Get(int id) { // id is 0 here if you pass string. return Ok(id); } }
1- Inline Constraints
app.UseMvc(routes => { routes.MapRoute("Values", "api/values/{id:int}", new { controller = "Values", action = "Get" }); });
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{name:minlength(2):maxlength(10):alpha}")] public IActionResult Get(string name) { return Ok(name); } }
2- MapRoute's Constraints Argument
app.UseMvc(routes => { routes.MapRoute( name: "Values", template: "api/values/{name}", defaults: new { controller = "Values", action = "Get" }, constraints: new { name = new CompositeRouteConstraint(new List<IRouteConstraint> { new AlphaRouteConstraint(), new MinLengthRouteConstraint(2), new MaxLengthRouteConstraint(10) }) }); });
ایجاد یک Constraint سفارشی
public class StartsWithConstraint : IRouteConstraint { public StartsWithConstraint(string startsWith) { if (string.IsNullOrWhiteSpace(startsWith)) throw new ArgumentNullException(nameof(StartsWith)); StartsWith = startsWith; } private string StartsWith { get; } public bool Match(HttpContext httpContext, IRouter route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { if (parameterName == null) throw new ArgumentNullException(nameof(parameterName)); if (values == null) throw new ArgumentNullException(nameof(values)); if (!values.TryGetValue(parameterName, out var value) || value == null) return false; string valueString = Convert.ToString(value, CultureInfo.InvariantCulture); return valueString.StartsWith(StartsWith); } }
services.Configure<RouteOptions>(opt => opt.ConstraintMap.Add("startsWith", typeof(StartsWithConstraint)));
[Route("api/[controller]")] public class ValuesController : ControllerBase { [HttpGet("{name:minlength(2):maxlength(10):alpha:startsWith(Mo)}")] public IActionResult Get(string name) { return Ok(name); } }
پس از آن، برای تبدیل صفحات یک فایل PDF به تصویر، مراحل زیر باید طی شود:
الف) وهله سازی از شیء AcroExch.PDDoc
در صورتیکه SDK یاد شده بر روی سیستم نصب نباشد، این وهله سازی با شکست مواجه خواهد شد و همچنین باید دقت داشت که این SDK به همراه نگارش رایگان Adobe reader ارائه نمیشود.
ب) گشودن فایل PDF به کمک شیء Com وهله سازی شده (pdfDoc.Open)
ج) دریافت اطلاعات صفحه مورد نظر (pdfDoc.AcquirePage)
د) کپی این اطلاعات به درون clipboard ویندوز (pdfPage.CopyToClipboard)
به این ترتیب به یک تصویر Bmp قرار گرفته شده در clipboard ویندوز خواهیم رسید
ه) مرحله بعد تغییر ابعاد و ذخیره سازی این تصویر نهایی است.
کدهای زیر، روش انجام این مراحل را بیان میکنند:
using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using Acrobat; //Add a Com Object ref. to "Adobe Acrobat 10.0 Type Library" => Program Files\Adobe\Acrobat 10.0\Acrobat\acrobat.tlb using Microsoft.Win32; namespace PdfThumbnail.Lib { public static class PdfToImage { const string AdobeObjectsErrorMessage = "Failed to create the PDF object."; const string BadFileErrorMessage = "Failed to open the PDF file."; const string ClipboardError = "Failed to get the image from clipboard."; const string SdkError = "This operation needs the Acrobat SDK(http://www.adobe.com/devnet/acrobat/downloads.html), which is combined with the full version of Adobe Acrobat."; public static byte[] PdfPageToPng(string pdfFilePath, int thumbWidth = 600, int thumbHeight = 750, int pageNumber = 0) { byte[] imageData = null; runJob((pdfDoc, pdfRect) => { imageData = pdfPageToPng(thumbWidth, thumbHeight, pageNumber, pdfDoc, pdfRect); }, pdfFilePath); return imageData; } public static void AllPdfPagesToPng(Action<byte[], int, int> dataCallback, string pdfFilePath, int thumbWidth = 600, int thumbHeight = 750) { runJob((pdfDoc, pdfRect) => { var numPages = pdfDoc.GetNumPages(); for (var pageNumber = 0; pageNumber < numPages; pageNumber++) { var imageData = pdfPageToPng(thumbWidth, thumbHeight, pageNumber, pdfDoc, pdfRect); dataCallback(imageData, pageNumber + 1, numPages); } }, pdfFilePath); } static void runJob(Action<CAcroPDDoc, CAcroRect> job, string pdfFilePath) { if (!File.Exists(pdfFilePath)) throw new InvalidOperationException(BadFileErrorMessage); var acrobatPdfDocType = Type.GetTypeFromProgID("AcroExch.PDDoc"); if (acrobatPdfDocType == null || !isAdobeSdkInstalled) throw new InvalidOperationException(SdkError); var pdfDoc = (CAcroPDDoc)Activator.CreateInstance(acrobatPdfDocType); if (pdfDoc == null) throw new InvalidOperationException(AdobeObjectsErrorMessage); var acrobatPdfRectType = Type.GetTypeFromProgID("AcroExch.Rect"); var pdfRect = (CAcroRect)Activator.CreateInstance(acrobatPdfRectType); var result = pdfDoc.Open(pdfFilePath); if (!result) throw new InvalidOperationException(BadFileErrorMessage); job(pdfDoc, pdfRect); releaseComObjects(pdfDoc, pdfRect); } public static byte[] ResizeImage(this Image image, int thumbWidth, int thumbHeight) { var srcWidth = image.Width; var srcHeight = image.Height; using (var bmp = new Bitmap(thumbWidth, thumbHeight, PixelFormat.Format32bppArgb)) { using (var gr = Graphics.FromImage(bmp)) { gr.SmoothingMode = SmoothingMode.HighQuality; gr.PixelOffsetMode = PixelOffsetMode.HighQuality; gr.CompositingQuality = CompositingQuality.HighQuality; gr.InterpolationMode = InterpolationMode.High; var rectDestination = new Rectangle(0, 0, thumbWidth, thumbHeight); gr.DrawImage(image, rectDestination, 0, 0, srcWidth, srcHeight, GraphicsUnit.Pixel); using (var memStream = new MemoryStream()) { bmp.Save(memStream, ImageFormat.Png); return memStream.ToArray(); } } } } static bool isAdobeSdkInstalled { get { return Registry.ClassesRoot.OpenSubKey("AcroExch.PDDoc", writable: false) != null; } } private static Bitmap pdfPageToBitmap(int pageNumber, CAcroPDDoc pdfDoc, CAcroRect pdfRect) { var pdfPage = (CAcroPDPage)pdfDoc.AcquirePage(pageNumber); if (pdfPage == null) throw new InvalidOperationException(BadFileErrorMessage); var pdfPoint = (CAcroPoint)pdfPage.GetSize(); pdfRect.Left = 0; pdfRect.right = pdfPoint.x; pdfRect.Top = 0; pdfRect.bottom = pdfPoint.y; pdfPage.CopyToClipboard(pdfRect, 0, 0, 100); Bitmap pdfBitmap = null; var thread = new Thread(() => { var data = Clipboard.GetDataObject(); if (data != null && data.GetDataPresent(DataFormats.Bitmap)) pdfBitmap = (Bitmap)data.GetData(DataFormats.Bitmap); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); Marshal.ReleaseComObject(pdfPage); return pdfBitmap; } private static byte[] pdfPageToPng(int thumbWidth, int thumbHeight, int pageNumber, CAcroPDDoc pdfDoc, CAcroRect pdfRect) { var pdfBitmap = pdfPageToBitmap(pageNumber, pdfDoc, pdfRect); if (pdfBitmap == null) throw new InvalidOperationException(ClipboardError); var pdfImage = pdfBitmap.GetThumbnailImage(thumbWidth, thumbHeight, null, IntPtr.Zero); // (+ 7 for template border) var imageData = pdfImage.ResizeImage(thumbWidth + 7, thumbHeight + 7); return imageData; } private static void releaseComObjects(CAcroPDDoc pdfDoc, CAcroRect pdfRect) { pdfDoc.Close(); Marshal.ReleaseComObject(pdfRect); Marshal.ReleaseComObject(pdfDoc); } } }
using System; using System.IO; using System.Windows.Forms; using PdfThumbnail.Lib; namespace PdfThumbnail { class Program { static void Main(string[] args) { var pdfPath = Application.StartupPath + @"\test.pdf"; PdfToImage.AllPdfPagesToPng((pageImageData, pageNumber, numPages) => { Console.WriteLine("Page {0}/{1}", pageNumber, numPages); File.WriteAllBytes(string.Format("{0}\\page-{1}.png", Application.StartupPath, pageNumber), pageImageData); }, pdfPath); } } }
کدهای این قسمت را از اینجا نیز میتوانید دریافت کنید:
PdfThumbnail.zip
در این مقاله حالتهایی را که الگوی طراحی Null Object، قادر به تشخیص آنها نیست را به وسیله الگوی طراحی Special case رفع میکنیم.
گاهی از اوقات پیش خواهد آمد که ما تسلیم شده و شیءای را برمیگردانیم که خودش نشان دهنده خطاست. اما همه شکستها یکسان نیستند. در ابتدا، کاربر ممکن است واجد شرایط انجام خرید نباشد. اما اگر واجد شرایط باشد، آیتم مورد نظر ممکن است در انبار نباشد. در صورت موجود بودن در انبار، حساب کاربر ممکن است مبلغ کافی نداشته باشد.
بجای برگشت دادن شیء Null در تمام این موارد، ما میتوانیم نتیجه را اصلاح کنیم و اساسا هر بار یک شیء مختلف را بازگردانیم. اینها هنوز هم نوعی از اشیاء Null هستند؛ ولی اینبار دارای معانی هستند. یکی از انها برای «حساب کاربری ناکافی» است، یکی دیگر برای «سایت در دست تعمیر و نگهداری» است و یا یکی دیگر از آنها «موجود نبودن در انبار» خواهد بود.
چنین اشیائی به موارد خاص ( Special Case ) اشاره میکنند. ما میتوانیم اشیاء مورد خاص ( Special Case ) را به عنوان نتایج واقعی عملیات بسازیم. فقط اگر تمام بررسیهای کسب و کار به پایان برسند و عملیات موفقیت آمیز باشد، آنگاه شیء واقعی نتیجه را باز میگردانیم.
نمونهای از پیاده سازی موارد خاص
این بخشی از رابط کاربری است که توسط سرویسهای برنامه کاربردی اجرا میشود:
public interface IApplicationServices { ... IReceiptViewModel LoggedInUserPurchase(string itemName); }
لایه نمایش انتظار دارد که لایه نرم افزار یک ویو مدل به خصوصی را برای آن تولید کند. در حال حاضر ما یک سناریوی موفقیت آمیز را داریم که در آن ویو مدل شامل اطلاعات واقعی از خرید است و چندین سناریوی شکست.
اگرعملیات خرید با هر کدام از شرایط زیر مواجه شود به شکست میانجامد:
1) سایت در دست تعمیر و نگهداری باشد.
2) کاربر ثبت نشده و یا فعال نیست.
3) آیتمی موجود نیست و یا وجود ندارد.
4) موجودی کاربر کم باشد.
public class DownForMaintenance: IReceiptViewModel { }
public class InvalidUser: IReceiptViewModel { public string UserName { get; private set; } public InvalidUser(string userName) { this.UserName = userName; } }
public class OutOfStock: IReceiptViewModel { public string UserName { get; private set; } public string ItemName { get; private set; } public OutOfStock(string userName, string itemName) { this.UserName = userName; this.ItemName = itemName; } }
public class InsufficientFunds: IReceiptViewModel { public string UserName { get; private set; } public decimal Amount { get; private set; } public string ItemName { get; private set; } public InsufficientFunds(string userName, decimal amount, string itemName) { this.UserName = userName; this.Amount = amount; this.ItemName = itemName; } }
public class ApplicationServices: IApplicationServices { ... public IReceiptViewModel LoggedInUserPurchase(string itemName) { if (IsDownForMaintenance()) return new DownForMaintenance(); return this.domain.Purchase(Session.LoggedInUserName, itemName); } private bool IsDownForMaintenance() { return File.Exists("maintenance.lock"); } }
public class DomainServices: IDomainServices { public IReceiptViewModel Purchase(string userName, string itemName) { User user = this.userRepository.Find(userName); if (user == null) return new InvalidUser(userName); Account account = this.accountRepository.FindByUser(user); return this.Purchase(user, account, itemName); } private IReceiptViewModel Purchase(User user, Account account, string itemName) { Product item = this.productRepository.Find(itemName); if (item == null) return new OutOfStock(user.UserName, itemName); ReceiptDto receipt = user.Purchase(item); MoneyTransaction transaction = account.Withdraw(receipt.Price); if (transaction == null) return new InsufficientFunds(user.UserName, receipt.Price, itemName); return receipt; } }
mysite.com/reporting
using System.ComponentModel.DataAnnotations; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] public string ChannelUrl { get; set; } } }
using System.Collections.Generic; namespace SampleProject.Models { public static class ChannelDataSource { static ChannelDataSource() => Channels = new List<Channel>(); public static List<Channel> Channels { get; private set; } public static void Add(Channel channel) => Channels.Add(channel); } }
using SampleProject.Models; using System.Linq; using System.Web.Mvc; namespace SampleProject.Controllers { public class ChannelController : Controller { // GET: Channel public ActionResult Index() { var channels = ChannelDataSource.Channels; return View(channels); } public ActionResult Channel(string channelUrl) { if (string.IsNullOrWhiteSpace(channelUrl)) { return new HttpNotFoundResult("channel not found!"); } var channel = ChannelDataSource.Channels.SingleOrDefault(ch => ch.ChannelUrl == channelUrl.ToLower()); if (channel == null) { return new HttpNotFoundResult("channel not found!"); } return View(channel); } public ActionResult Create() => View(); [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(Channel channel) { if (!ModelState.IsValid) { ModelState.AddModelError(string.Empty, "Please check your inputs!"); return View(channel); } ChannelDataSource.Add(channel); TempData["Message"] = "Channel added successfully!"; return RedirectToAction(nameof(Index)); } } }
using System.Web.Mvc; using System.Web.Routing; namespace SampleProject { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "ChannelUrls", url: "{channelurl}", defaults: new { controller = "Channel", action = "Channel", id = UrlParameter.Optional } ); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Channel", action = "Index", id = UrlParameter.Optional } ); } } }
using System.Collections.Generic; using System.Linq; using System.Web.Routing; namespace SampleProject.Models { public static class Utility { public static List<string> GetAllAreaNames() { var areaNames = RouteTable.Routes.OfType<Route>() .Where(d => d.DataTokens != null) .Where(d=> d.DataTokens.ContainsKey("area")) .Select(r => r.DataTokens["area"].ToString().ToLower()) .ToList(); return areaNames; } } }
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web.Mvc; using SampleProject.Models; namespace SampleProject.CustomValidators { public class CheckForAreaExisting : ValidationAttribute, IClientValidatable { public List<string> AreaNames { get { return Utility.GetAllAreaNames(); } } public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context) { var rule = new ModelClientValidationRule { ValidationType = "checkforareaexisting", ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()) }; rule.ValidationParameters.Add("areanames", string.Join(",", AreaNames)); yield return rule; } public override bool IsValid(object value) { if (value != null) { return Utility.GetAllAreaNames() .SingleOrDefault(area => area == value.ToString().ToLower()) == null; } return true; } } }
jQuery.validator.addMethod("checkforareaexisting", function (value, element, param) { var isValueOneOfTheAreaNames = $.inArray(value.toLowerCase(), param.areaNames) === -1; return isValueOneOfTheAreaNames; }); $.validator.unobtrusive.adapters.add('checkforareaexisting', ['areanames'], function (options) { options.rules['checkforareaexisting'] = { areaNames: options.params.areanames.split(',') }; options.messages['checkforareaexisting'] = options.message; });
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>_Layout</title> <style> </style> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/Jquery.js"></script> <script src="~/Scripts/jquery.validate.min.js"></script> <script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script> <script src="~/Scripts/CustomValidation.js"></script> </body> </html>
using SampleProject.CustomValidators; using System.ComponentModel.DataAnnotations; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] [CheckForAreaExisting(ErrorMessage = "You can't use this url for your channel!")] public string ChannelUrl { get; set; } } }
[HttpPost] public ActionResult CheckForAreaExisting(string channelUrl) { var isValueOneOfTheAreaNames = Utility.GetAllAreaNames() .SingleOrDefault(area => area == channelUrl.ToLower()) == null; return Json(isValueOneOfTheAreaNames); }
using System.ComponentModel.DataAnnotations; using System.Web.Mvc; namespace SampleProject.Models { public class Channel { public string ChannelTitle { get; set; } [Required] [Remote("CheckForAreaExisting", "Channel", ErrorMessage = "You can't use this url for your channel!", HttpMethod = "Post")] public string ChannelUrl { get; set; } } }
- صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
- فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC
- سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
- آشنایی با کتابخانهی PDF Report
اضافه کردن دکمهی خروجی به jqGrid
برای تهیه خروجی از jqGrid نیاز است بدانیم، اکنون در چه صفحهای از اطلاعات قرار داریم؟ بر روی چه ستونی، مرتب سازی صورت گرفتهاست؟ بر روی کدام فیلدها با چه مقادیری جستجو انجام شدهاست؟ تا ... بتوانیم بر این مبنا، منبع دادهی موجود را فیلتر کرده و لیست نهایی را تبدیل به گزارش کنیم. گزارشی که دقیقا با اطلاعاتی که کاربر در صفحه مشاهده میکند، تطابق داشته باشد.
خوشبختانه تمام این سؤالات توسط متد توکار excelExport در سمت سرور قابل دریافت است:
@section Scripts { <script type="text/javascript"> $(document).ready(function () { $('#list').jqGrid({ caption: "آزمایش ششم", // مانند قبل }).navGrid( // مانند قبل }).jqGrid('navButtonAdd', '#pager', { caption: "", buttonicon: "ui-icon-print", title: "خروجی پی دی اف", onClickButton: function () { $("#list").jqGrid('excelExport', { url: '@Url.Action("GetProducts", "Home")' }); } }); }); </script> }
در اینجا توسط متد navButtonAdd یک دکمهی جدید را اضافه کردهایم که کلیک بر روی آن سبب فراخوانی متد excelExport و ارسال اطلاعات گزارش به url تنظیم شدهاست. باید دقت داشت که این اطلاعات از طریق Http Get به سرور ارسال میشوند و دقیقا اجزای آن همان اجزای جستجوی پویای jqGrid است:
public ActionResult GetProducts(string sidx, string sord, int page, int rows, bool _search, string searchField, string searchString, string searchOper, string filters, string oper)
البته چون تعداد این پارامترها بیش از اندازه شدهاست، بهتر است آنها را تبدیل به یک کلاس کرد:
namespace jqGrid06.Models { public class JqGridRequest { public string sidx { set; get; } public string sord { set; get; } public int page { set; get; } public int rows { set; get; } public bool _search { set; get; } public string searchField { set; get; } public string searchString { set; get; } public string searchOper { set; get; } public string filters { set; get; } public string oper { set; get; } } }
public ActionResult GetProducts(JqGridRequest request) { var list = ProductDataSource.LatestProducts; var pageIndex = request.page - 1; var pageSize = request.rows; var totalRecords = list.Count; var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize); var productsQuery = list.AsQueryable(); productsQuery = new JqGridSearch().ApplyFilter(productsQuery, request, this.Request.Form); productsQuery = productsQuery.OrderBy(request.sidx + " " + request.sord); if (string.IsNullOrWhiteSpace(request.oper)) { productsQuery = productsQuery .Skip(pageIndex * pageSize) .Take(pageSize); } else if (request.oper == "excel") { productsQuery = productsQuery .Skip(pageIndex * pageSize); } var productsList = productsQuery.ToList(); if (!string.IsNullOrWhiteSpace(request.oper) && request.oper == "excel") { new ProductsPdfReport().CreatePdfReport(productsList); } var productsData = new JqGridData { Total = totalPages, Page = request.page, Records = totalRecords, Rows = (productsList.Select(product => new JqGridRowData { Id = product.Id, RowCells = new List<string> { product.Id.ToString(CultureInfo.InvariantCulture), product.Name, product.AddDate.ToPersianDate(), product.Price.ToString(CultureInfo.InvariantCulture) } })).ToArray() }; return Json(productsData, JsonRequestBehavior.AllowGet); }
توضیحات:
اکثر قسمتهای این متد با متدی که در مطلب «فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC» مشاهده کردید یکی است؛ برای مثال order by آن با استفاده از کتابخانهی Dynamic LINQ به صورت پویا عمل میکند و متد ApplyFilter، کار تهیه where پویا را انجام میدهد.
فقط در اینجا بررسی و پردازش پارامتر oper نیز اضافه شدهاست. اگر این پارامتر مقدار دهی شده باشد، یعنی نیاز است کل اطلاعات را واکشی کرد؛ زیرا میخواهیم گزارش گیری کنیم و نه اینکه صرفا اطلاعات یک صفحه را به کاربر بازگشت دهیم. همچنین در اینجا List نهایی فیلتر شده به یک گزارش Pdf Report ارسال میشود. این گزارش چون نهایتا اطلاعات را در مرورگر کاربر Flush میکند، کار به اجرای سایر قسمتها نخواهد رسید و همینجا گزارش نهایی تهیه میشود.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
jqGrid06.7z
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
فرمت کردن اطلاعات نمایش داده شده به کمک jqGrid در ASP.NET MVC
استفاده ازExpressionها جهت ایجاد Strongly typed view در ASP.NET MVC
فرمهای پویای jqGrid نیز به صورت Ajax ایی به سرور ارسال میشوند و اگر نوع عناصر تشکیل دهندهی آنها file تعیین شوند، قادر به ارسال این فایلها به سرور نخواهند بود. در ادامه نحوهی یکپارچه سازی افزونهی AjaxFileUpload را با فرمهای jqGrid بررسی خواهیم کرد.
تغییرات فایل Layout برنامه
در اینجا دو فایل جدید ajaxfileupload.js و jquery.blockUI.js به مجموعهی فایلهای تعریف شده اضافه شدهاند:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" /> <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" /> <link href="~/Content/Site.css" rel="stylesheet" type="text/css" /> </head> <body> <div> @RenderBody() </div> <script src="~/Scripts/jquery-1.7.2.min.js"></script> <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script> <script src="~/Scripts/i18n/grid.locale-fa.js"></script> <script src="~/Scripts/jquery.jqGrid.src.js"></script> <script src="~/Scripts/ajaxfileupload.js"></script> <script src="~/Scripts/jquery.blockUI.js"></script> @RenderSection("Scripts", required: false) </body> </html>
PM> Install-Package jQuery.BlockUI
نکتهای در مورد واکنشگرا کردن jqGrid
اگر میخواهید عرض jqGrid به تغییرات اندازهی مرورگر پاسخ دهد، تنها کافی است تغییرات ذیل را اعمال کنید:
<div dir="rtl" id="grid1" style="width:100%;" align="center"> <div id="rsperror"></div> <table id="list" cellpadding="0" cellspacing="0"></table> <div id="pager" style="text-align:center;"></div> </div> <script type="text/javascript"> $(document).ready(function () { // Responsive jqGrid $(window).bind('resize', function () { var targetContainer = "#grid1"; var targetGrid = "#list"; $(targetGrid).setGridWidth($(targetContainer).width() - 2, true); }).trigger('resize'); $('#list').jqGrid({ caption: "آزمایش هفتم", /// ..... }).navGrid( /// ..... ).jqGrid('gridResize', { minWidth: 400, minHeight: 150 }); }); </script>
همچنین اگر میخواهید کاربر بتواند اندازهی گرید را دستی تغییر دهد، به انتهای تعاریف گرید، تعریف متد gridResize را نیز اضافه کنید.
نحوهی تعریف ستونی که قرار است فایل آپلود کند
colModel: [ { name: '@(StronglyTyped.PropertyName<Product>(x=>x.ImageName))', index: '@(StronglyTyped.PropertyName<Product>(x => x.ImageName))', align: 'center', width: 220, editable: true, edittype: 'file', formatter: function (cellvalue, options, rowObject) { return "<img src='/images/" + cellvalue + "?rnd=" + new Date().getTime() + "' />"; }, unformat: function (cellvalue, options, cell) { return $('img', cell).attr('src').replace('/images/', ''); } } ],
Rnd اضافه شده به انتهای آدرس تصویر، جهت جلوگیری از کش شدن آن تعریف شدهاست.
کتابخانهی JqGridHelper
در قسمتهای قبل مطالب بررسی jqGrid یک سری کلاس مانند JqGridData برای بازگشت اطلاعات مخصوص jqGrid و یا JqGridRequest برای دریافت پارامترهای ارسالی توسط آن به سرور، تهیه کردیم؛ به همراه کلاسهایی مانند جستجو و مرتب سازی پویای اطلاعات.
اگر این کلاسها را از پروژهها و مثالهای ارائه شده خارج کنیم، میتوان به کتابخانهی JqGridHelper رسید که فایلهای آن در پروژهی پیوست موجود هستند.
همچنین در این پروژه، کلاسی به نام StronglyTyped با متد PropertyName جهت دریافت نام رشتهای یک خاصیت تعریف شدهاست. گاهی از اوقات این تنها چیزی است که کدهای سمت کلاینت، جهت سازگار شدن با Refactoring و Strongly typed تعریف شدن نیاز دارند و نه ... محصور کنندههایی طویل و عریض که هیچگاه نمیتوانند تمام قابلیتهای یک کتابخانهی غنی جاوا اسکریپتی را به همراه داشته باشند.
با کمی جستجو، برای jqGrid نیز میتوانید از این دست محصور کنندههارا پیدا کنید اما ... هیچکدام کامل نیستند و دست آخر مجبور خواهید شد در بسیاری از موارد مستقیما JavaScript نویسی کنید.
یکپارچه سازی افزونهی AjaxFileUpload با فرمهای jqGrid
پس از این مقدمات، ستون ویژهی actions که inline edit را فعال میکند، چنین تعریفی را پیدا خواهد کرد:
colModel: [ { name: 'myac', width: 80, fixed: true, sortable: false, resize: false, formatter: 'actions', formatoptions: { keys: true, afterSave: function (rowid, response) { doInlineUpload(response, rowid); }, delbutton: true, delOptions: { url: "@Url.Action("DeleteProduct","Home")" } } } ],
و ویژگیهای قسمتهای edit، add و delete فرمهای پویای jqGrid باید به نحو ذیل تغییر کنند:
$('#list').jqGrid({ caption: "آزمایش هفتم", // .... }).navGrid( '#pager', //enabling buttons { add: true, del: true, edit: true, search: false }, //edit option { width: 'auto', reloadAfterSubmit: true, checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); }, afterSubmit: doFormUpload, closeAfterEdit: true }, //add options { width: 'auto', url: '@Url.Action("AddProduct","Home")', reloadAfterSubmit: true, checkOnUpdate: true, checkOnSubmit: true, beforeShowForm: function (form) { centerDialog(form, $('#list')); }, afterSubmit: doFormUpload, closeAfterAdd: true }, //delete options { url: '@Url.Action("DeleteProduct","Home")', reloadAfterSubmit: true }).jqGrid('gridResize', { minWidth: 400, minHeight: 150 });
افزونهی AjaxFileUpload پس از ارسال اطلاعات عناصر غیر فایلی فرم، باید فعال شود. به همین جهت است که از رویداد afterSubmit در حالت نمایش فرمهای پویا و رویداد afterSave در حالت ویرایش inline استفاده کردهایم.
در ادامه تعاریف متدهای doInlineUpload و doUpload بکار گرفته شده در رویداد afterSubmit را مشاهده میکنید:
function doInlineUpload(response, rowId) { return doUpload(response, null, rowId); } function doFormUpload(response, postdata) { return doUpload(response, postdata, null); } function doUpload(response, postdata, rowId) { // دریافت خروجی متد ثبت اطلاعات از سرور // و استفاده از آی دی رکورد ثبت شده برای انتساب فایل آپلودی به آن رکورد var result = $.parseJSON(response.responseText); if (result.success === false) return [false, "عملیات ثبت موفقیت آمیز نبود", result.id]; var fileElementId = '@(StronglyTyped.PropertyName<Product>(x=>x.ImageName))'; if (rowId) { fileElementId = rowId + "_" + fileElementId; } var val = $("#" + fileElementId).val(); if (val == '' || val === undefined) { // فایلی انتخاب نشده return [false, "لطفا فایلی را انتخاب کنید", result.id]; } $('#grid1').block({ message: '<h4>در حال ارسال فایل به سرور</h4>' }); $.ajaxFileUpload({ url: "@Url.Action("UploadFiles", "Home")", // مسیری که باید فایل به آن ارسال شود secureuri: false, fileElementId: fileElementId, // آی دی المان ورودی فایل dataType: 'json', data: { id: result.id }, // اطلاعات اضافی در صورت نیاز complete: function () { $('#grid1').unblock(); }, success: function (data, status) { $("#list").trigger("reloadGrid"); }, error: function (data, status, e) { alert(e); } }); return [true, "با تشکر!", result.id]; }
متد doUpload توسط پارامتر response، اطلاعات بازگشتی پس از ذخیره سازی متداول اطلاعات فرم را دریافت میکند. برای مثال ابتدا اطلاعات معمولی یک محصول در بانک اطلاعاتی ذخیره شده و سپس id آن به همراه یک خاصیت به نام success از طرف سرور بازگشت داده میشوند.
اگر success مساوی true بود، ادامهی کار آپلود فایل انجام خواهد شد. در اینجا ابتدا بررسی میشود که آیا فایلی از طرف کاربر انتخاب شدهاست یا خیر؟ اگر خیر، یک پیام اعتبارسنجی سفارشی به او نمایش داده خواهد شد.
خروجی متد doUpload حتما باید به شکل یک آرایه سه عضوی باشد. عضو اول آن true و false است؛ به معنای موفقیت یا عدم موفقیت عملیات. عضو دوم پیام اعتبارسنجی سفارشی است و عضو سوم، Id ردیف.
در ادامه افزونهی jQuery.BlockUI فعال میشود تا ارسال فایل به سرور را به کاربر گوشزد کند.
سپس فراخوانی متداول افزونهی ajaxFileUpload را مشاهده میکنید. تنها نکتهی مهم آن فراخوانی متد reloadGrid در حالت success است. به این ترتیب گرید را وادار میکنیم تا اطلاعات ذخیره شده در سمت سرور را دریافت کرده و سپس تصویر را به نحو صحیحی نمایش دهد.
کدهای سمت سرور آپلود فایل
[HttpPost] public ActionResult AddProduct(Product postData) { // ... return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult EditProduct(Product postData) { // ... return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet); } // todo: change `imageName` according to the form's file element name [HttpPost] public ActionResult UploadFiles(HttpPostedFileBase imageName, int id) { // .... return Json(new { FileName = product.ImageName }, "text/html", JsonRequestBehavior.AllowGet); }
در حالتهای Add و Edit، نیاز است id رکورد ثبت شده بازگشت داده شود. این id در سمت کلاینت توسط پارامتر response دریافت میشود. از آن در افزونهی ارسال فایل به سرور استفاده خواهیم کرد. اگر به متد UploadFiles دقت کنید، این id را دریافت میکند. بنابراین میتوان یک ربط منطقی را بین فایل ارسالی و رکورد متناظر با آن برقرار کرد.
Content type مقدار بازگشتی از متد UploadFiles حتما باید text/html باشد (افزونهی ارسال فایلها، اینگونه کار میکند).
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
jqGrid07.zip
معرفی System.Text.Json در NET Core 3.0.
فرض کنید مدلی را به این صورت تعریف کردهاید:
public class ModelIdViewModel { public string Id { set; get; } }
public async Task<IActionResult> RenderRole([FromBody]ModelIdViewModel model)
در سمت کلاینت نیز اطلاعات Ajax ای متناظر با آنرا به صورت زیر ارسال میکنید:
data: JSON.stringify({ "id": 1 }), contentType: "application/json; charset=utf-8", dataType: "json",
برای دیباگ آن اگر قطعه کد زیر را اضافه کنیم:
public async Task<IActionResult> RenderRole([FromBody]ModelIdViewModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); }
{"$.id":["The JSON value could not be converted to System.String. Path: $.id | LineNumber: 0 | BytePositionInLine: 7."]}
برای رفع این مشکل، فقط کافی است نوع Id را در model به int تبدیل کرد:
public class ModelIdViewModel { public int Id { set; get; } }