مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
if (args.CellType == CellType.PreviousPageSummaryCell || args.CellType == CellType.PageSummaryCell || args.CellType == CellType.SummaryRowCell) { if (args.TableRowData != null && args.Cell.RowData.PropertyName == "CaclulatedDetection") { var summaryRowCredit = args.TableRowData.FirstOrDefault(x => x.PropertyName == "Creditor"); var summaryRowDebit = args.TableRowData.FirstOrDefault(x => x.PropertyName == "Debtor"); if (summaryRowCredit != null && summaryRowDebit != null) { args.Cell.RowData.FormattedValue = (summaryRowCredit.PropertyValue.ToSafeDouble() - summaryRowDebit.PropertyValue.ToSafeDouble()) > 0 ? "بستانکار" : "بدهکار"; } } }
doc.RunDirection(PdfRunDirection.LeftToRight);
در واقع فکر میکنم وقتی گزارش رو به صورت RightToLeft هم تنظیم میکنیم بازم از سمت چپ شروع میکنه به render کردن سلولهای یک سطر.
این روزها توسعه دهندگان مرورگرها، شمارههای نگارشهای برنامههای خود را مرتبا و با اعداد نجومی بالا میبرند. امروز فایرفاکس 5، تا چند وقت دیگر فایرفاکس 8 هم در راه خواهد بود؛ سایر مرورگرها نیز به همین ترتیب. به همراه نگارشهای متفاوت دات نت، یک سری browser definition files نیز وجود دارد که تنها بازهی محدودی از این شماره نگارشها در آنها تعریف شده است. در نتیجه زمانیکه مثلا مرورگر کاربر IE 10 باشد، آنرا به عنوان unknown browser تشخیص داده و یک سری از قابلیتها را به صورت خودکار حذف میکند. برای مثال جاوا اسکریپت برای این نوع مرورگرها خاموش میشود. منظور از خاموش کردن جاوا اسکریپت این است که موتور ASP.NET Webforms دیگر یک سری اسکریپتهای تولیدی مخصوص خود را به صفحه تزریق نخواهد کرد؛ برای مثال تمام PostBackهای سایت شما از کار خواهند افتاد و مسایلی از این دست.
روش حل آن هم ساده است. باید فایلهای firefox.browser و ie.browser به روز شده را در پوشهی استاندارد App_Browsers کپی کنید و همین!
به زودی یک آپدیت دات نت نیز جهت به روز رسانی در سطح ماشین، ارائه خواهد شد. فعلا مجبور هستید به ازای تک تک برنامههای خود این پوشهی به روز شده را کپی کنید.
جهت توضیحات بیشتر و دریافت فایلهای ذکر شده به این مطلب مراجعه نمائید.
- میخواهیم footer پیش فرض PdfReport را که تاریخ را در یک سمت، و شماره صفحه را در سمتی دیگر نمایش میدهد، به عبارت «صفحه x از n» تغییر دهیم.
- میخواهیم در Header گزارش بجای Header پیش فرض PdfReport یکی از قالبهای PDF تهیه شده توسط Open Office را نمایش دهیم (و یا هر ساختار دیگری را).
تمام اجزای PdfReport جهت امکان اعمال تغییرات کلی و توسعه آنها طراحی شدهاند؛ قالبها، هدر، فوتر، منابع داده، قالبهای نمایش سلولها، تعریف توابع تجمعی سفارشی و غیره. جهت سهولت کار، به ازای هر یک از این موارد، پیاده سازیهای پیش فرضی در PdfReport قرار دارند، امکان اگر مورد رضایت شما نیستند ... از بنیان تغییرشان دهید! (و همچنین اگر مورد جالبی را پیاده سازی کردید، میتوانید به عنوان یک وصله جدید ارائه دهید تا به پروژه اضافه شود)
ضمنا این مطالب سفارشی سازی نیاز به آشنایی با ساختار iTextSharp را نیز دارند؛ در حد ایجاد یک جدول ساده باید با iTextSharp آشنا باشید.
مدلهای مورد استفاده:
namespace PdfReportSamples.Models { public class Task { public int Id { set; get; } public string Name { set; get; } public int PercentCompleted { set; get; } public bool IsActive { set; get; } public User Assignee { set; get; } } }
using System; namespace PdfReportSamples.Models { public class User { public int Id { set; get; } public string Name { set; get; } public string LastName { set; get; } public long Balance { set; get; } public DateTime RegisterDate { set; get; } } }
column.PropertyName<Task>(x => x.Assignee.Name)
using System; using System.Collections.Generic; using System.Drawing; using PdfReportSamples.Models; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.CustomHeaderFooter { public class CustomHeaderFooterPdfReport { readonly CustomHeader _customHeader = new CustomHeader(); public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight)); }) .PagesHeader(header => { header.CustomHeader(_customHeader); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.SilverTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.MultipleColumnsPerPage(new MultipleColumnsPerPage { ColumnsGap = 22, ColumnsPerPage = 2, ColumnsWidth = 250, IsRightToLeft = false, TopMargin = 7 }); }) .MainTableDataSource(dataSource => { var rows = new List<Task>(); var rnd = new Random(); for (int i = 1; i < 210; i++) { rows.Add(new Task { Assignee = new User { Id = i, Name = "user-" + i }, IsActive = rnd.Next(0, 2) == 1 ? true : false, Name = "task-" + i }); } dataSource.StronglyTypedList(rows); }) .MainTableColumns(columns => { columns.AddColumn(column => { column.PropertyName("rowNo"); column.IsRowNumber(true); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(0); column.Width(1); column.HeaderCell("#"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.Name); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(3); column.HeaderCell("Task Name"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.Assignee.Name); // nested property support column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(3); column.HeaderCell("Assignee"); }); columns.AddColumn(column => { column.PropertyName<Task>(x => x.IsActive); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(2); column.HeaderCell("Active"); column.ColumnItemsTemplate(template => { template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed); }); }); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .Export(export => { export.ToExcel(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\CustomHeaderFooterPdfReportSample.pdf")); } } }
به همراه Header سفارشی:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; namespace PdfReportSamples.CustomHeaderFooter { public class CustomHeader : IPageHeader { public PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> rowdata, IList<SummaryCellData> summaryData) { return null; } Image _image; public PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData) { if (_image == null) //cache is empty { var templatePath = AppPath.ApplicationPath + "\\data\\PdfHeaderTemplate.pdf"; _image = PdfImageHelper.GetITextSharpImageFromPdfTemplate(pdfWriter, templatePath); } var table = new PdfPTable(1); var cell = new PdfPCell(_image, true) { Border = 0 }; table.AddCell(cell); return table; } } }
و Footer سفارشی استفاده شده:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Contracts; namespace PdfReportSamples.CustomHeaderFooter { public class CustomFooter : IPageFooter { PdfContentByte _pdfContentByte; readonly IPdfFont _pdfRptFont; readonly Font _font; readonly PdfRunDirection _direction; PdfTemplate _template; public CustomFooter(IPdfFont pdfRptFont, PdfRunDirection direction) { _direction = direction; _pdfRptFont = pdfRptFont; _font = _pdfRptFont.Fonts[0]; } public void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData) { _template.BeginText(); _template.SetFontAndSize(_pdfRptFont.Fonts[0].BaseFont, 8); _template.SetTextMatrix(0, 0); _template.ShowText((writer.PageNumber - 1).ToString()); _template.EndText(); } public void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData) { var pageSize = document.PageSize; var text = "Page " + writer.PageNumber + " / "; var textLen = _font.BaseFont.GetWidthPoint(text, _font.Size); var center = (pageSize.Left + pageSize.Right) / 2; var align = _direction == PdfRunDirection.RightToLeft ? Element.ALIGN_RIGHT : Element.ALIGN_LEFT; ColumnText.ShowTextAligned( canvas: _pdfContentByte, alignment: align, phrase: new Phrase(text, _font), x: center, y: pageSize.GetBottom(25), rotation: 0, runDirection: (int)_direction, arabicOptions: 0); var x = _direction == PdfRunDirection.RightToLeft ? center - textLen : center + textLen; _pdfContentByte.AddTemplate(_template, x, pageSize.GetBottom(25)); } public void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData) { _pdfContentByte = writer.DirectContent; _template = _pdfContentByte.CreateTemplate(50, 50); } } }
البته لازم به ذکر است که تمام این کدها به پوشه Samples سورس پروژه نیز جهت سهولت دسترسی، اضافه شدهاند .
توضیحات:
برای پیاده سازی Header و Footer سفارشی در PdfReport نیاز خواهید داشت تا دو اینترفیس IPageHeader و IPageFooter را پیاده سازی کنید.
ساختار IPageHeader را در ذیل ملاحظه میکنید:
using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; namespace PdfRpt.Core.Contracts { public interface IPageHeader { PdfPTable RenderingGroupHeader(Document pdfDoc, PdfWriter pdfWriter, IList<CellData> newGroupInfo, IList<SummaryCellData> summaryData); PdfPTable RenderingReportHeader(Document pdfDoc, PdfWriter pdfWriter, IList<SummaryCellData> summaryData); } }
RenderingGroupHeader مرتبط است به مباحث گروه بندی اطلاعات و گزارشات master-detail که در قسمتهای بعد به آنها اشاره خواهد شد. چون در اینجا به آن نیازی نداشتیم، تنها کافی است متد متناظر با آن، null بر گرداند که در کلاس CustomHeader فوق قابل مشاهده است.
متد RenderingReportHeader به ازای تولید هر صفحه جدید، فراخوانی خواهد شد. به عبارتی میتوانید در صفحات مختلف، هدرهای مختلفی را نمایش دهید.
خروجی هر دو متد در اینجا یک جدول از نوع PdfPTable است. بنابراین هر نوع ساختار دلخواهی را که علاقمند هستید به شکل یک PdfPTable ایجاد کرده و بازگشت دهید. این جدول در هدر صفحات ظاهر خواهد شد.
برای نمونه در کلاس CustomHeader، یک قالب تهیه شده توسط Open Office توسط متد توکار PdfImageHelper.GetITextSharpImageFromPdfTemplate دریافت و تبدیل به تصویر میشود. این تصویر از نوع تصاویر قابل درک توسط iTextSharp است و نه اینکه واقعا تبدیل به یک تصویر معمولی مثلا از نوع bmp شود. سپس این تصویر، در یک ردیف از جدولی قرار داده شده و این جدول بازگشت داده میشود.
در کل یا توسط کار با PdfPTable میتوانید یک هدر غیرپیش فرض را طراحی کنید و یا میتوانید توسط ابزارهای بصری مانند Open Office یک قالب خاص را برای آن تهیه کرده و به روشی که ذکر شد و کدهای آنرا ملاحظه میکنید، بارگذاری و استفاده کنید. این قالبها در مسیر Bin\Data سورسهای پروژه قرار داده شدهاند.
ساختار IPageFooter به صورت زیر است:
using iTextSharp.text; using iTextSharp.text.pdf; using System.Collections.Generic; namespace PdfRpt.Core.Contracts { public interface IPageFooter { void DocumentOpened(PdfWriter writer, IList<SummaryCellData> columnCellsSummaryData); void PageFinished(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData); void ClosingDocument(PdfWriter writer, Document document, IList<SummaryCellData> columnCellsSummaryData); } }
برای طراحی یک Footer سفارشی کافی است اینترفیس فوق را پیاده سازی کنید که نمونهای از آنرا در کدهای کلاس CustomFooter ملاحظه مینمائید.
متد DocumentOpened، با وهله سازی شیء Document فراخوانی میشود.
متد PageFinished هر بار پیش از اتمام کار صفحه جاری و افزوده شدن آن به Document فراخوانی میگردد.
متد ClosingDocument، در زمان بسته شدن شیء Document فراخوانی خواهد شد.
اگر به امضای این متدها دقت کنید، شیء PdfWriter در اختیار شما قرار گرفته است که توسط آن میتوان مستقیما بر روی فایل PDF، محتوایی را قرار داد. شیء Document نیز در دسترس است. مثلا توسط آن میتوان اندازه دقیق صفحه را بدست آورد.
به علاوه پارامتر columnCellsSummaryData نیز امکان دسترسی به مقادیر ردیفهای قبلی را در اختیار شما قرار میدهد. برای مثال اگر نیاز دارید تا بر اساس مقادیر ستونها و ردیفهای قبلی، محاسباتی را انجام داده و در پایین صفحات درج کنید، به این ترتیب دسترسی کاملی به آنها، خواهید داشت.
استفاده از این کلاسهای سفارشی نیز همواره به شکل زیر خواهد بود:
readonly CustomHeader _customHeader = new CustomHeader(); //... .PagesFooter(footer => { footer.CustomFooter(new CustomFooter(footer.PdfFont, PdfRunDirection.LeftToRight)); }) .PagesHeader(header => { header.CustomHeader(_customHeader); })
مثال 1: چه تعداد امکانات، توسط این مجموعه ارائه میشود؟
var count = context.Facilities.Count();
مثال 2: چه تعداد امکانات گران قیمتی توسط این مجموعه ارائه میشود؟
میخواهیم تعداد امکاناتی را بیابیم که guestcost آنها بزرگتر یا مساوی 10 است.
var count = context.Facilities.Count(x => x.GuestCost >= 10);
مثال 3: هر کاربر چه تعدادی کاربر دیگری را توصیه کردهاست؟
خروجی این گزارش بر اساس recommendedby و count باشد و مرتب شدهی بر اساس ID افراد.
var members = context.Members .Where(member => member.RecommendedBy != null) .GroupBy(member => member.RecommendedBy) .Select(group => new { RecommendedBy = group.Key, Count = group.Count() }) .OrderBy(result => result.RecommendedBy) .ToList();
مثال 4: تعداد slots رزرو شدهی به ازای هر کدام از امکانات موجود را نمایش دهید.
جهت یادآوری از قسمت اول: «هر رزرو کردن مکان و امکاناتی در این مجموعه، «نیم ساعته» است. بنابراین Slots در اینجا به معنای تعداد نیم ساعتهای رزرو کردن یک مکان خاص است؛ که به آن «half hour slots» نیز گفته میشود و زمان شروع این رزرو نیز ثبت میشود.»
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شدهی بر اساس ID هر امکان موجود.
var facilities = context.Bookings .GroupBy(booking => booking.FacId) .Select(group => new { FacId = group.Key, TotalSlots = group.Sum(booking => booking.Slots) }) .OrderBy(result => result.FacId) .ToList();
مثال 5: تعداد slots رزرو شدهی در ماه September 2012 را به ازای هر کدام از امکانات موجود، نمایش دهید.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شدهی بر اساس ID تعداد slots.
var date1 = new DateTime(2012, 09, 01); var date2 = new DateTime(2012, 10, 01); var facilities = context.Bookings .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2) .GroupBy(booking => booking.FacId) .Select(group => new { FacId = group.Key, TotalSlots = group.Sum(booking => booking.Slots) }) .OrderBy(result => result.TotalSlots) .ToList();
مثال 6: محاسبه کنید در سال 2012 و به ازای هر ماه مجزای آن، چه تعداد slots رزرو شدهاند.
خروجی این گزارش بر اساس facid, month, Total Slots باشد و مرتب شدهی بر اساس ID و شمارهی ماه.
var date1 = new DateTime(2012, 01, 01); var date2 = new DateTime(2013, 01, 01); var facilities = context.Bookings .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2) .GroupBy(booking => new { booking.FacId, booking.StartTime.Month }) .Select(group => new { group.Key.FacId, group.Key.Month, TotalSlots = group.Sum(booking => booking.Slots) }) .OrderBy(result => result.FacId) .ThenBy(result => result.Month) .ToList();
الف) میتوان گروه بندی را بر روی بیش از یک ستون اعمال کرد. در این حالت در Select بعدی، group.Key به کل شیء گروه بندی شده، اشاره میکند.
ب) روش انتخاب ماه میلادی از یک خاصیت DateTime و گروه بندی بر اساس آن
که به صورت زیر ترجمه میشود:
مثال 7: چه تعداد کاربر مجموعه، حداقل یکبار امکاناتی را رزرو کردهاند؟
var count = context.Bookings.Select(booking => booking.MemId).Distinct().Count();
- (*)COUNT یعنی بازگشت تعداد ردیفهای نهایی گزارش.
- COUNT(address) یعنی بازگشت تعداد آدرسهای غیرنال، در کل ردیفهای نهایی گزارش.
- COUNT(DISTINCT address) یعنی بازگشت تعداد آدرسهای غیرمشابه در کل ردیفهای نهایی گزارش.
COUNT DISTINCT را EF-Core به صورت ترکیبی از یک sub-query ترجمه میکند:
مثال 8: امکاناتی را لیست کنید که بیش از 1000 slots رزرو شده دارند.
خروجی این گزارش بر اساس facid و Total Slots باشد و مرتب شدهی بر اساس ID هر امکان موجود.
var facilities = context.Bookings .GroupBy(booking => booking.FacId) .Select(group => new { FacId = group.Key, TotalSlots = group.Sum(booking => booking.Slots) }) .Where(result => result.TotalSlots > 1000) .OrderBy(result => result.FacId) .ToList();
مثال 9: میزان فروش کل هر امکان موجود را محاسبه کنید.
خروجی این گزارش بر اساس name, revenue باشد و مرتب شدهی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
var facilities = context.Bookings.Select(booking => new { booking.Facility.Name, Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => b.Name) .Select(group => new { Name = group.Key, TotalRevenue = group.Sum(b => b.Revenue) }) .OrderBy(result => result.TotalRevenue) .ToList();
مثال 10: کدامیک از امکانات موجود، میزان فروشی کمتر از 1000 داشتهاند؟
خروجی این گزارش بر اساس name, revenue باشد و مرتب شدهی بر اساس میزان فروش. بخاطر داشته باشید که میزان فروش کاربران ثبت نام شده با کاربران مهمان یکی نیست.
var facilities = context.Bookings.Select(booking => new { booking.Facility.Name, Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => b.Name) .Select(group => new { Name = group.Key, TotalRevenue = group.Sum(b => b.Revenue) }) .Where(result => result.TotalRevenue < 1000) .OrderBy(result => result.TotalRevenue) .ToList();
مثال 11: کدامیک از امکانات موجود، بیشترین slots رزرو شده را دارد؟
var item = context.Bookings .GroupBy(booking => booking.FacId) .Select(group => new { FacId = group.Key, TotalSlots = group.Sum(booking => booking.Slots) }) .OrderByDescending(result => result.TotalSlots) .FirstOrDefault();
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.
- صفحه بندی و مرتب سازی خودکار اطلاعات به کمک 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
export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" }, { _id: "5b21ca3eeb7f6fbccd471820", name: "Thriller" } ]; export function getGenres() { return genres.filter(g => g); }
بررسی ساختار کامپوننت ListGroup
شبیه به کامپوننت صفحه بندی که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت ListGroup نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\listGroup.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import ListGroup from "./common/listGroup";
برای این منظور ابتدا React.Fragment موجود را با یک div با "className="row جایگزین میکنیم. سپس داخل این row، دو ستون را تعریف خواهیم کرد که در اولی، المان جدید ListGroup قرار میگیرد و در دومی، مابقی عناصری که تاکنون اضافه کردهایم؛ مانند جدول، صفحه بندی و نمایش تعداد آیتمها:
return ( <div className="row"> <div className="col-2"> <ListGroup /> </div> <div className="col"> ... </div> </div> );
import { getGenres } from "../services/fakeGenreService"; // ... class Movies extends Component { state = { // ... genres: getGenres() };
class Movies extends Component { state = { movies: [], pageSize: 4, currentPage: 1, genres: [] }; componentDidMount() { this.setState({ movies: getMovies(), genres: getGenres() }); }
پس از آن میتوان ویژگی جدید items این کامپوننت را به آرایهی genres دریافتی از state، تنظیم کرد:
<ListGroup items={this.state.genres} />
بهتر است هر زمانیکه کاربر، آیتمی را از این لیست انتخاب کرد، توسط بروز رخدادی مانند onItemSelect از وقوع آن مطلع شد و سپس نسبت به آن توسط متد handleGenreSelect، واکنش نشان داد؛ مانند فیلتر کردن لیست فیلمها بر اساس آیتم انتخابی و نمایش آن. به همین جهت ویژگی onItemSelect را به تعریف المان ListGroup اضافه میکنیم:
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); };
پیاده سازی نمایش آیتمها در کامپوننت ListGroup
پیاده سازی ابتدایی کامپوننت ListGroup را در اینجا مشاهده میکنید:
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item._id} className="list-group-item"> {item.name} </li> ))} </ul> ); } } export default ListGroup;
تا اینجا اگر برنامه را ذخیره کرده و در مرورگر نمایش دهیم، به خروجی زیر میرسیم:
البته به نظر عرض ستون آن نامناسب است. به همین جهت به کامپوننت movies مراجعه کرده و col-2 ستون آنرا به col-3 تبدیل میکنیم.
پویا سازی انتخاب نام خواص شیء دریافتی، در کامپوننت ListGroup
در حال حاضر پیاده سازی کامپوننت ListGroup، به شیءای دقیقا با خواص id_ و name وابستهاست و اگر شیء دیگری را که دارای خواصی معادل این نامها نیست، به آن ارسال کنیم، دیگر کار نخواهد کرد. به همین جهت در محل تعریف المان این کامپوننت در کامپوننت movies، دو ویژگی دیگر نام خواص شیء مدنظر را تنظیم میکنیم تا بتوانیم با هر نوع شیءای در اینجا کار کنیم:
<ListGroup items={this.state.genres} textProperty="name" valueProperty="_id" onItemSelect={this.handleGenreSelect} />
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item[this.props.valueProperty]} className="list-group-item"> {item[this.props.textProperty]} </li> ))} </ul> ); } } export default ListGroup;
تعیین مقادیر پیشفرضی برای خواص props
با زیاد شدن تعداد خواص props، اینترفیس کامپوننتها پیچیدهتر میشوند. در یک چنین حالتی میتوان در کامپوننتها defaultProps را تعریف کرد و توسط آن مقادیر پیشفرضی را برای خواص props درنظر گرفت. به این صورت در حین تعریف المان این کامپوننت، اگر مقادیر مدنظر با مقادیر پیشفرض تعیین شده یکی باشند، دیگر نیازی به ذکر این پارامترها نخواهد بود. برای مثال در انتهای کامپوننت ListGroup، خاصیت جدید defaultProps را تعریف میکنیم (املای آن باید دقیقا به همین شکل باشد؛ و گرنه شناخته نخواهد شد). سپس در اینجا خواصی را که میخواهیم مقادیر پیشفرضی را برای آنها تعیین کنیم، ذکر خواهیم کرد:
ListGroup.defaultProps = { textProperty: "name", valueProperty: "_id" }; export default ListGroup;
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
مدیریت انتخاب گروههای فیلمها
در ادامه میخواهیم رخداد onClick بر روی هر li این لیست را مدیریت کنیم و سبب بروز رخدادی به نام onItemSelect شویم که در ابتدای بحث، آنرا به عنوان خروجی این کامپوننت تعریف کردیم. این رخداد نیز در کامپوننت movies به متد handleGenreSelect متصل است. به همین جهت تعریف ویژگی onClick را که سبب انتقال شیء جاری رندر شده، توسط رویداد onItemSelect به خارج از آن میشود، به المان li کامپوننت ListGroup اضافه میکنیم:
<li key={item[this.props.valueProperty]} className="list-group-item" onClick={() => this.props.onItemSelect(item)} style={{ cursor: "pointer" }} > {item[this.props.textProperty]} </li>
پس از فعالسازی امکان کلیک بر روی هر آیتم لیست رندر شده، اکنون میخواهیم با انتخاب هر گروه، این گروه در این لیست، به صورت انتخاب شده، همانند شماره صفحهی انتخاب شدهی در کامپوننت صفحه بندی، تغییر رنگ دهد و متمایز نمایش داده شود تا مشخص باشد که هم اکنون با کدام آیتم در حال کار هستیم. برای اینکار تنها کافی است کلاس active را به صورت پویا به className هر li، اضافه یا کم کنیم. البته برای این منظور این کامپوننت باید از آیتم انتخاب شده مطلع باشد؛ به همین جهت selectedItem را در لیست ویژگیهای اینترفیس تعریف این المان اضافه میکنیم. برای اینکار ابتدا selectedGenre را با هربار فراخوانی handleGenreSelect که به onItemSelect کامپوننت متصل است، با فراخوانی متد setState به روز رسانی میکنیم:
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({selectedGenre: genre}); };
class Movies extends Component { state = { // ... selectedGenre: {} };
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} selectedItem={this.state.selectedGenre} />
<li key={item[this.props.valueProperty]} className={ item === this.props.selectedItem ? "list-group-item active" : "list-group-item" } style={{ cursor: "pointer" }} onClick={() => this.props.onItemSelect(item)} > {item[this.props.textProperty]} </li>
مدیریت فیلتر کردن اطلاعات گروه فیلم انتخابی
در قسمت قبل، در ابتدای متد رندر کامپوننت movies، از متد paginate برای صفحه بندی اطلاعات استفاده کردیم. فیلتر گروه جاری انتخاب شده را باید پیش از این متد قرار دارد؛ چون تعداد صفحات و اطلاعات نمایش داده شدهی در هر کدام باید بر اساس لیست فیلمهای فیلتر شده باشد.
برای انجام اینکار تغییرات زیر را اعمال خواهیم کرد:
الف) بجای متد paginate، از متد getPagedData زیر استفاده میکنیم:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies } = this.state; const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies; const first = (currentPage - 1) * pageSize; const last = first + pageSize; const pagedMovies = filteredMovies.slice(first, last); return { totalCount: filteredMovies.length, data: pagedMovies }; }
- در حین Object Destructuring، نام خاصیت movies را نیز به allMovies تغییر دادهایم تا واضحتر باشد.
- در ادامه با استفاده از متد filter جاوااسکریپت، بر اساس id هر گروه انتخاب شده، اشیاء مرتبط با آن، از allMovies جدا شده و بازگشت داده میشود. البته اگر id هم انتخاب نشده باشد (اولین بار نمایش صفحه)، تمام رکوردها یعنی allMovies، مورد استفاده قرار میگیرد.
- پس از آن، همان کدهای صفحه بندی اطلاعات را که در قسمت قبل بررسی کردیم، مشاهده میکنید که اینبار بجای allMovies قسمت قبل، بر روی filteredMovies اعمال شدهاست.
- در آخر، این متد، یک شیء را با دو خاصیت که بیانگر تعداد کل رکوردهای انتخاب شده و دادههای فیلتر شدهی صفحه بندی شدهاست، بازگشت میدهد.
ب) تغییرات متد رندر کامپوننت movies به صورت زیر است:
- ابتدا متد getPagedData فوق، فراخوانی شده و شیء دریافتی از آن با استفاده از ویژگی Object Destructuring، به دو خاصیت totalCount و movies انتساب داده میشود:
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const { totalCount, data: movies } = this.getPagedData();
- همچنین کامپوننت صفحه بندی، اینبار باید totalCount آیتمهای فیلتر شده را نمایش دهد و نه totalCount تمام فیلمهای موجود را:
<Pagination itemsCount={totalCount}
<p>Showing {totalCount} movies in the database.</p>
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, currentPage: 1 }); };
افزودن گزینهی نمایش تمام اطلاعات به لیست گروههای فیلمها
در ادامه قصد داریم به بالای لیست گروههای موجود، گزینهی All Genres را نیز اضافه کنیم تا با کلیک بر روی آن، مجددا بتوان لیست تمام فیلمهای موجود را مشاهده کرد.
برای این منظور در جائیکه لیست getGenres را دریافت و نمایش میدهیم، یعنی متد componentDidMount، اندکی تغییر ایجاد کرده و یک آرایهی جدید را ایجاد میکنیم؛ بطوریکه اولین عنصر آن، گزینهی جدید All Genres باشد و سپس توسط spread operator، مابقی عناصر آرایهی گروهها را به این آرایهی جدید اضافه میکنیم:
componentDidMount() { const genres = [{ _id: "", name: "All Genres" }, ...getGenres()]; this.setState({ movies: getMovies(), genres }); }
const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies;
فـقـط اگه برای اعداد هم فارسی میشد کامل بود . اگر مقدوره قابلیت فارسی سازی اعداد رو هم اضافه کنید .
ویژگیهای مورد بحث شامل موارد زیر میباشند:
• ویرایش 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
تغییراتی در آن اعمال نمود.
ادامه دارد ...