من به جای Controller از apiController استفاده کردم اما وقتی خطای زیر را میده:
JSON.parse: unexpected character at line 1 column 1 of the JSON data
JSON.parse: unexpected character at line 1 column 1 of the JSON data
در مطلب «تولید پویای ستونها در PdfReport» عنوان شد که ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. همچنین به کمک متد MainTableAdHocColumnsConventions میتوان بر اساس نوعهای دادهای، بر روی نحوه نمایش ستونها تاثیر گذاشت. برای مثال هرجایی DateTime مشاهده شد، به صورت خودکار تبدیل به تاریخ شمسی شود.
روش دیگری که این روزها در اکثر فریمهای دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیتهای تعریف شده کلاسها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده میکند (در تولید پویای کد، یا اعتبار سنجیهای سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدلهای اینگونه فریم ورکها، اکثر ویژگیها و Data Annotations متداول را نیز میتوان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آنها خواهیم پرداخت.
آشنایی با مدلهای بکار رفته در مثال جاری:
در اینجا یک enum، جهت تعیین سمت شغلی تعریف شده است. برای اینکه بتوان خروجی مطلوبی را در گزارشات شاهد بود، میتوان از ویژگی Description، جهت تعیین مقدار نمایشی آنها نیز استفاده کرد و این تعاریف در PdfReport خوانده و اعمال میشوند.
مدل فوق جهت مقدار دهی اطلاعات یک شخص تعریف شده است.
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسبهای سرستونها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالبهای ستونهای سفارشی و یا حتی قالبهای پیش فرض دیگری که متنی نیستند، وجود دارد، میتوانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاسهای پیش فرض قالبهای ستونها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا میتوان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونهای از استفاده از آنرا برای تعیین نحوه نمایش تاریخ، ملاحظه میکنید. توسط این ویژگی حتی میتوان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آنها نیز پیش بینی شده است که در قسمتهای بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه میکنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمیتوان توسط ویژگیها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)
در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
در پایان، نحوه استفاده از منبع داده فوق جهت تامین یک گزارش، به نحو زیر میباشد:
همانطور که مشخص است، از ذکر متد MainTableColumns به علت استفاده از DataAnnotations صرفنظر شده و PdfReport این تعاریف را بر اساس ویژگیهای خواص کلاس شخص دریافت میکند. تنها از متد MainTableAdHocColumnsConventions جهت مشخص سازی اینکه نیاز به نمایش ستون ردیف میباشد، استفاده کردهایم.
روش دیگری که این روزها در اکثر فریمهای دات نتی مرسوم شده است، استفاده از Data Annotations جهت انتساب یک سری متادیتا به خاصیتهای تعریف شده کلاسها است. برای مثال ASP.NET MVC از این قابلیت زیاد استفاده میکند (در تولید پویای کد، یا اعتبار سنجیهای سمت سرور و کاربر).
به همین جهت برای سازگاری بیشتر PdfReport با مدلهای اینگونه فریم ورکها، اکثر ویژگیها و Data Annotations متداول را نیز میتوان در PdfReport بکار برد. همچنین تعدادی ویژگی سفارشی نیز تعریف شده است، که در ادامه به بررسی آنها خواهیم پرداخت.
آشنایی با مدلهای بکار رفته در مثال جاری:
using System.ComponentModel; namespace PdfReportSamples.Models { public enum JobTitle { [Description("Grunt")] Grunt, [Description("Programmer")] Programmer, [Description("Analyst Programmer")] AnalystProgrammer, [Description("Project Manager")] ProjectManager, [Description("Chief Information Officer")] ChiefInformationOfficer, } }
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using PdfReportSamples.Models; using PdfRpt.Aggregates.Numbers; using PdfRpt.ColumnsItemsTemplates; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; using PdfRpt.DataAnnotations; namespace PdfReportSamples.DataAnnotations { public class Person { [IsVisible(false)] public int Id { get; set; } [DisplayName("User name")] //Note: If you don't specify the ColumnItemsTemplate, a new TextBlockField() will be used automatically. [ColumnItemsTemplate(typeof(TextBlockField))] public string Name { get; set; } [DisplayName("Job title")] public JobTitle JobTitle { set; get; } [DisplayName("Date of birth")] [DisplayFormat(DataFormatString = "{0:MM/dd/yyyy}")] public DateTime DateOfBirth { get; set; } [DisplayName("Date of death")] [DisplayFormat(NullDisplayText = "-", DataFormatString = "{0:MM/dd/yyyy}")] public DateTime? DateOfDeath { get; set; } [DisplayFormat(DataFormatString = "{0:n0}")] [CustomAggregateFunction(typeof(Sum))] public int Salary { get; set; } [IsCalculatedField(true)] [DisplayName("Calculated Field")] [DisplayFormat(DataFormatString = "{0:n0}")] [AggregateFunction(AggregateFunction.Sum)] public string CalculatedField { get; set; } [CalculatedFieldFormula("CalculatedField")] public static Func<IList<CellData>, object> CalculatedFieldFormula = list => { if (list == null) return string.Empty; var salary = (int)list.GetValueOf<Person>(x => x.Salary); return salary * 0.8; };//Note: It's a static field, not a property. } }
- اگر قصد ندارید خاصیتی در این بین، در گزارشات ظاهر شود، از ویژگی IsVisible با مقدار false استفاده کنید.
- از ویژگی DisplayName جهت تعیین برچسبهای سرستونها استفاده خواهد شد.
- ذکر ویژگی ColumnItemsTemplate اختیاری است و اگر عنوان نشود به صورت خودکار از TextBlockField استفاده خواهد شد. اما اگر نیاز به استفاده از قالبهای ستونهای سفارشی و یا حتی قالبهای پیش فرض دیگری که متنی نیستند، وجود دارد، میتوانید از ویژگی ColumnItemsTemplate به همراه نوع کلاس مورد نظر استفاده نمائید. کلاسهای پیش فرض قالبهای ستونها در PdfReport در پوشه Lib\ColumnsItemsTemplates سورس آن قرار دارند.
- برای تعیین نحوه فرمت اطلاعات در اینجا میتوان از ویژگی DisplayFormat استفاده کرد. این ویژگی در اسمبلی System.ComponentModel.DataAnnotations.dll دات نت تعریف شده است؛ که در اینجا نمونهای از استفاده از آنرا برای تعیین نحوه نمایش تاریخ، ملاحظه میکنید. توسط این ویژگی حتی میتوان مشخص ساخت (توسط پارامتر NullDisplayText) که اگر اطلاعاتی null بود، بجای آن چه عبارتی نمایش داده شود.
- اگر علاقمند به اعمال تابعی تجمعی به ستونی خاص هستید، از ویژگی CustomAggregateFunction استفاده کنید. پارامتر آن نوع کلاس تابع مورد نظر است. یک سری تابع تجمعی پیش فرض در فضای نام PdfRpt.Aggregates.Numbers قرار دارند. البته امکان تهیه انواع سفارشی آنها نیز پیش بینی شده است که در قسمتهای بعد به آن خواهیم پرداخت.
- امکان تعریف خواص محاسباتی نیز پیش بینی شده است. برای این منظور دو کار را باید انجام داد:
الف) ویژگی IsCalculatedField را با مقدار true بر روی خاصیت مورد نظر اعمال کنید.
ب) هم نام خاصیت محاسباتی افزوده شده به کلاس جاری، ویژگی CalculatedFieldFormula را بر روی یک فیلد استاتیک عمومی در آن کلاس به نحوی که ملاحظه میکنید (مطابق امضای فیلد CalculatedFieldFormula فوق)، تعریف نمائید. (علت این است که نمیتوان توسط ویژگیها از delegates استفاده کرد و این محدودیت ذاتی وجود دارد)
در ادامه کدهای منبع داده فرضی مثال جاری ذکر شده است:
using System; using System.Collections.Generic; using PdfReportSamples.Models; namespace PdfReportSamples.DataAnnotations { public static class PersonnelDataSource { public static IList<Person> CreatePersonnelList() { return new List<Person> { new Person { Id = 1, Name = "Edward", DateOfBirth = new DateTime(1900, 1, 1), DateOfDeath = new DateTime(1990, 10, 15), JobTitle = JobTitle.ChiefInformationOfficer, Salary = 5000 }, new Person { Id = 2, Name = "Margaret", DateOfBirth = new DateTime(1950, 2, 9), DateOfDeath = null, JobTitle = JobTitle.AnalystProgrammer, Salary = 4000 }, new Person { Id = 3, Name = "Grant", DateOfBirth = new DateTime(1975, 6, 13), DateOfDeath = null, JobTitle = JobTitle.Programmer, Salary = 3500 } }; } } }
using System; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.DataAnnotations { public class DataAnnotationsPdfReport { 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.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("new rpt."); defaultHeader.RunDirection(PdfRunDirection.LeftToRight); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.FitToContent); }) .MainTableDataSource(dataSource => { dataSource.StronglyTypedList(PersonnelDataSource.CreatePersonnelList()); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) .MainTableSummarySettings(summary => { summary.OverallSummarySettings("Total"); summary.PageSummarySettings("Page Summary"); summary.PreviousPageSummarySettings("Pervious Page Summary"); }) .MainTableAdHocColumnsConventions(adHocColumns => { adHocColumns.ShowRowNumberColumn(true); adHocColumns.RowNumberColumnCaption("#"); }) .Export(export => { export.ToExcel(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\DataAnnotationsSampleRpt.pdf")); } } }
بهروزرسانی فایلهای Resource در زمان اجرا
یکی از ویژگیهای مهمی که در پیاده سازی محصول با استفاده از فایلهای Resource باید به آن توجه داشت، امکان بروز رسانی محتوای این فایلها در زمان اجراست. از آنجاکه احتمال اینکه کاربران سیستم خواهان تغییر این مقادیر باشند بسیار زیاد است، بنابراین درنظر گرفتن چنین ویژگیای برای محصول نهایی میتواند بسیار تعیین کننده باشد. متاسفانه پیاده سازی چنین امکانی درباره فایلهای Resource چندان آسان نیست. زیرا این فایلها همانطور که در قسمت قبل توضیح داده شد پس از کامپایل به صورت اسمبلیهای ستلایت (Satellite Assembly) درآمده و دیگر امکان تغییر محتوای آنها بصورت مستقیم و به آسانی وجود ندارد.نکته: البته نحوه پیاده سازی این فایلها در اسمبلی نهایی (و در حالت کلی نحوه استفاده از هر فایلی در اسمبلی نهایی) در ویژوال استودیو توسط خاصیت Build Action تعیین میشود. برای کسب اطلاعات بیشتر راجع به این خاصیت به اینجا رجوع کنید.
یکی از روشهای نسبتا مندرآوردی که برای ویرایش و به روزرسانی کلیدهای Resource وجود دارد بدین صورت است:
- ابتدا باید اصل فایلهای Resource به همراه پروژه پابلیش شود. بهترین مکان برای نگهداری این فایلها فولدر App_Data است. زیرا محتویات این فولدر توسط سیستم FCN (همان File Change Notification) در ASP.NET رصد نمیشود.
نکته: علت این حساسیت این است که FCN در ASP.NET تقریبا تمام محتویات فولدر سایت در سرور (فولدر App_Data یکی از معدود استثناهاست) را تحت نظر دارد و رفتار پیشفرض این است که با هر تغییری در این محتویات، AppDomain سایت Unload میشود که پس از اولین درخواست دوباره Load میشود. این اتفاق موجب از دست دادن تمام سشنها و محتوای کشها و ... میشود (اطلاعات بیشتر و کاملتر درباره نحوه رفتار FCN در اینجا).
- سپس با استفاده یک مقدار کدنویسی امکاناتی برای ویرایش محتوای این فایلها فراهم شود. ازآنجا که محتوای این فایلها به صورت XML ذخیره میشود بنابراین براحتی میتوان با امکانات موجود این ویژگی را پیاده سازی کرد. اما در فضای نام System.Windows.Forms کلاسهایی وجود دارد که مخصوص کار با این فایلها طراحی شده اند که کار نمایش و ویرایش محتوای فایلهای Resource را سادهتر میکند. به این کلاسها در قسمت قبلی اشاره کوتاهی شده بود.
- پس از ویرایش و به روزرسانی محتوای این فایلها باید کاری کنیم تا برنامه از این محتوای تغییر یافته به عنوان منبع جدید بهره بگیرد. اگر از این فایلهای Rsource به صورت embed استفاده شده باشد در هنگام build پروژه محتوای این فایلها به صورت Satellite Assembly در کنار کتابخانههای دیگر تولید میشود. اسمبلی مربوط به هر زبان هم در فولدری با عنوان زبان مربوطه ذخیره میشود. مسیر و نام فایل این اسمبلیها مثلا به صورت زیر است:
bin\fa\Resources.resources.dll
بنابراین در این روش برای استفاده از محتوای به روز رسانی شده باید عملیات Build این کتابخانه دوباره انجام شود و کتابخانههای جدیدی تولید شود. راه حل اولی که به ذهن میرسد این است که از ابزارهای پایه و اصلی برای تولید این کتابخانهها استفاده شود. این ابزارها (همانطور که در قسمت قبل نیز توضیح داده شد) عبارتند از Resource Generator و Assembly Linker. اما استفاده از این ابزارها و پیاده سازی روش مربوطه سختتر از آن است که به نظر میآید. خوشبختانه درون مجموعه عظیم دات نت ابزار مناسبتری برای این کار نیز وجود دارد که کار تولید کتابخانههای موردنظر را به سادگی انجام میدهد. این ابزار با عنوان Microsoft Build شناخته میشود که در اینجا توضیح داده شده است.
خواندن محتویات یک فایل resx.
همانطور که در بالا توضیح داده شد برای راحتی کار میتوان از کلاس زیر که در فایل System.Windows.Forms.dll قرار دارد استفاده کرد:
System.Resources.ResXResourceReader
این کلاس چندین کانستراکتور دارد که مسیر فایل resx. یا استریم مربوطه به همراه چند گزینه دیگر را به عنوان ورودی میگیرد. این کلاس یک Enumator دارد که یک شی از نوع IDictionaryEnumerator برمیگرداند. هر عضو این enumerator از نوع object است. برای استفاده از این اعضا ابتدا باید آنرا به نوع DictionaryEntry تبدیل کرد. مثلا بصورت زیر:
private void TestResXResourceReader() { using (var reader = new ResXResourceReader("Resource1.fa.resx")) { foreach (var item in reader) { var resource = (DictionaryEntry)item; Console.WriteLine("{0}: {1}", resource.Key, resource.Value); } } }
همانطور که ملاحظه میکنید استفاده از این کلاس بسیار ساده است. ازآنجاکه DictionaryEntry یک struct است، به عنوان یک راه حل مناسبتر بهتر است ابتدا کلاسی به صورت زیر تعریف شود:
public class ResXResourceEntry { public string Key { get; set; } public string Value { get; set; } public ResXResourceEntry() { } public ResXResourceEntry(object key, object value) { Key = key.ToString(); Value = value.ToString(); } public ResXResourceEntry(DictionaryEntry dictionaryEntry) { Key = dictionaryEntry.Key.ToString(); Value = dictionaryEntry.Value != null ? dictionaryEntry.Value.ToString() : string.Empty; } public DictionaryEntry ToDictionaryEntry() { return new DictionaryEntry(Key, Value); } }
سپس با استفاده از این کلاس خواهیم داشت:
private static List<ResXResourceEntry> Read(string filePath) { using (var reader = new ResXResourceReader(filePath)) { return reader.Cast<object>().Cast<DictionaryEntry>().Select(de => new ResXResourceEntry(de)).ToList(); } }
حال این متد برای استفادههای آتی آماده است.
نوشتن در فایل resx.
برای نوشتن در یک فایل resx. میتوان از کلاس ResXResourceWriter استفاده کرد. این کلاس نیز در کتابخانه System.Windows.Forms در فایل System.Windows.Forms.dll قرار دارد:
System.Resources.ResXResourceWriter
متاسفانه در این کلاس امکان افزودن یا ویرایش یک کلید به تنهایی وجود ندارد. بنابراین برای ویرایش یا اضافه کردن حتی یک کلید کل فایل باید دوباره تولید شود. برای استفاده از این کلاس نیز میتوان به شکل زیر عمل کرد:
private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath) { using (var writer = new ResXResourceWriter(filePath)) { foreach (var resource in resources) { writer.AddResource(resource.Key, resource.Value); } } }
در متد فوق از همان کلاس ResXResourceEntry که در قسمت قبل معرفی شد، استفاده شده است. از متد زیر نیز میتوان برای حالت کلی حذف یا ویرایش استفاده کرد:
private static void AddOrUpdate(ResXResourceEntry resource, string filePath) { var list = Read(filePath); var entry = list.SingleOrDefault(l => l.Key == resource.Key); if (entry == null) { list.Add(resource); } else { entry.Value = resource.Value; } Write(list, filePath); }
در این متد از متدهای Read و Write که در بالا نشان داده شدهاند استفاده شده است.
حذف یک کلید در فایل resx.
برای اینکار میتوان از متد زیر استفاده کرد:
private static void Remove(string key, string filePath) { var list = Read(filePath); list.RemoveAll(l => l.Key == key); Write(list, filePath); }
در این متد، از متد Write که در قسمت معرفی شد، استفاده شده است.
راه حل نهایی
قبل از بکارگیری روشهای معرفی شده در این مطلب بهتر است ابتدا یکسری قرارداد بصورت زیر تعریف شوند:
- طبق راهنماییهای موجود در قسمت قبل یک پروژه جداگانه با عنوان Resources برای نگهداری فایلهای resx. ایجاد شود.
- همواره آخرین نسخه از محتویات موردنیاز از پروژه Resources باید درون فولدری با عنوان Resources در پوشه App_Data قرار داشته باشد.
- آخرین نسخه تولیدی از محتویات موردنیاز پروژه Resource در فولدری با عنوان Defaults در مسیر App_Data\Resources برای فراهم کردن امکان "بازگرداندن به تنظیمات اولیه" وجود داشته باشد.
برای فراهم کردن این موارد بهترین راه حل استفاده از تنظیمات Post-build event command line است. اطلاعات بیشتر درباره Build Eventها در اینجا.
برای اینکار من از دستور xcopy استفاده کردم که نسخه توسعه یافته دستور copy است. دستورات استفاده شده در این قسمت عبارتند از:
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults /e /y /i /exclude:$(ProjectDir)excludes.txt
xcopy $(ProjectDir)$(OutDir)*.* $(SolutionDir)MvcApplication1\App_Data\Resources\Defaults\bin /e /y /i
در دستورات فوق آرگومان e/ برای کپی تمام فولدرها و زیرفولدرها، y/ برای تایید تمام کانفیرم ها، و i/ برای ایجاد خودکار فولدرهای موردنیاز استفاده میشود. آرگومان exclude/ نیز همانطور که از نامش پیداست برای خارج کردن فایلها و فولدرهای موردنظر از لیست کپی استفاده میشود. این آرگومان مسیر یک فایل متنی حاوی لیست این فایلها را دریافت میکند. در تصویر زیر یک نمونه از این فایل و مسیر و محتوای مناسب آن را مشاهده میکنید:
با استفاده از این فایل excludes.txt فولدرهای bin و obj و نیز فایلهای با پسوند user. و vspscc. (مربوط به TFS) و نیز خود فایل excludes.txt از لیست کپی دستور xcopy حذف میشوند و بنابراین کپی نمیشوند. درصورت نیاز میتوانید گزینههای دیگری نیز به این فایل اضافه کنید.
همانطور که در اینجا اشاره شده است، در تنظیمات Post-build event command line یکسری متغیرهای ازپیش تعریف شده (Macro) وجود دارند که از برخی از آنها در دستوارت فوق استفاده شده است:
(ProjectDir)$ : مسیر کامل و مطلق پروژه جاری به همراه یک کاراکتر \ در انتها
(SolutionDir)$ : مسیر کامل و مطلق سولوشن به همراه یک کاراکتر \ در انتها
(OutDir)$ : مسیر نسبی فولدر Output پروژه جاری به همراه یک کاراکتر \ در انتها
نکته: این دستورات باید در Post-Build Event پروژه Resources افزوده شوند.
با استفاده از این تنظیمات مطمئن میشویم که پس از هر Build آخرین نسخه از فایلهای موردنیاز در مسیرهای تعیین شده کپی میشوند. درنهایت با استفاده از کلاس ResXResourceManager که در زیر آورده شده است، کل عملیات را ساماندهی میکنیم:
public class ResXResourceManager { private static readonly object Lock = new object(); public string ResourcesPath { get; private set; } public ResXResourceManager(string resourcesPath) { ResourcesPath = resourcesPath; } public IEnumerable<ResXResourceEntry> GetAllResources(string resourceCategory) { var resourceFilePath = GetResourceFilePath(resourceCategory); return Read(resourceFilePath); } public void AddOrUpdateResource(ResXResourceEntry resource, string resourceCategory) { var resourceFilePath = GetResourceFilePath(resourceCategory); AddOrUpdate(resource, resourceFilePath); } public void DeleteResource(string key, string resourceCategory) { var resourceFilePath = GetResourceFilePath(resourceCategory); Remove(key, resourceFilePath); } private string GetResourceFilePath(string resourceCategory) { var extension = Thread.CurrentThread.CurrentUICulture.TwoLetterISOLanguageName == "en" ? ".resx" : ".fa.resx"; var resourceFilePath = Path.Combine(ResourcesPath, resourceCategory.Replace(".", "\\") + extension); return resourceFilePath; } private static void AddOrUpdate(ResXResourceEntry resource, string filePath) { var list = Read(filePath); var entry = list.SingleOrDefault(l => l.Key == resource.Key); if (entry == null) { list.Add(resource); } else { entry.Value = resource.Value; } Write(list, filePath); } private static void Remove(string key, string filePath) { var list = Read(filePath); list.RemoveAll(l => l.Key == key); Write(list, filePath); } private static List<ResXResourceEntry> Read(string filePath) { lock (Lock) { using (var reader = new ResXResourceReader(filePath)) { var list = reader.Cast<object>().Cast<DictionaryEntry>().ToList(); return list.Select(l => new ResXResourceEntry(l)).ToList(); } } } private static void Write(IEnumerable<ResXResourceEntry> resources, string filePath) { lock (Lock) { using (var writer = new ResXResourceWriter(filePath)) { foreach (var resource in resources) { writer.AddResource(resource.Key, resource.Value); } } } } }
در این کلاس تغییراتی در متدهای معرفی شده در قسمتهای بالا برای مدیریت دسترسی همزمان با استفاده از بلاک lock ایجاد شده است.
با استفاده از کلاس BuildManager عملیات تولید کتابخانهها مدیریت میشود. (در مورد نحوه استفاده از MSBuild در اینجا توضیحات کافی آورده شده است):public class BuildManager { public string ProjectPath { get; private set; } public BuildManager(string projectPath) { ProjectPath = projectPath; } public void Build() { var regKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\MSBuild\ToolsVersions\4.0"); if (regKey == null) return; var msBuildExeFilePath = Path.Combine(regKey.GetValue("MSBuildToolsPath").ToString(), "MSBuild.exe"); var startInfo = new ProcessStartInfo { FileName = msBuildExeFilePath, Arguments = ProjectPath, WindowStyle = ProcessWindowStyle.Hidden }; var process = Process.Start(startInfo); process.WaitForExit(); } }
درنهایت مثلا با استفاده از کلاس ResXResourceFileManager مدیریت فایلهای این کتابخانهها صورت میپذیرد:
public class ResXResourceFileManager { public static readonly string BinPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase.Replace("file:///", "")); public static readonly string ResourcesPath = Path.Combine(BinPath, @"..\App_Data\Resources"); public static readonly string ResourceProjectPath = Path.Combine(ResourcesPath, "Resources.csproj"); public static readonly string DefaultsPath = Path.Combine(ResourcesPath, "Defaults"); public static void CopyDlls() { File.Copy(Path.Combine(ResourcesPath, @"bin\debug\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true); File.Copy(Path.Combine(ResourcesPath, @"bin\debug\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true); Directory.Delete(Path.Combine(ResourcesPath, "bin"), true); Directory.Delete(Path.Combine(ResourcesPath, "obj"), true); } public static void RestoreAll() { RestoreDlls(); RestoreResourceFiles(); } public static void RestoreDlls() { File.Copy(Path.Combine(DefaultsPath, @"bin\Resources.dll"), Path.Combine(BinPath, "Resources.dll"), true); File.Copy(Path.Combine(DefaultsPath, @"bin\fa\Resources.resources.dll"), Path.Combine(BinPath, @"fa\Resources.resources.dll"), true); } public static void RestoreResourceFiles(string resourceCategory) { RestoreFile(resourceCategory.Replace(".", "\\")); } public static void RestoreResourceFiles() { RestoreFile(@"Global\Configs"); RestoreFile(@"Global\Exceptions"); RestoreFile(@"Global\Paths"); RestoreFile(@"Global\Texts"); RestoreFile(@"ViewModels\Employees"); RestoreFile(@"ViewModels\LogOn"); RestoreFile(@"ViewModels\Settings"); RestoreFile(@"Views\Employees"); RestoreFile(@"Views\LogOn"); RestoreFile(@"Views\Settings"); } private static void RestoreFile(string subPath) { File.Copy(Path.Combine(DefaultsPath, subPath + ".resx"), Path.Combine(ResourcesPath, subPath + ".resx"), true); File.Copy(Path.Combine(DefaultsPath, subPath + ".fa.resx"), Path.Combine(ResourcesPath, subPath + ".fa.resx"), true); } }
در این کلاس از مفهومی با عنوان resourceCategory برای استفاده راحتتر در ویوها استفاده شده است که بیانگر فضای نام نسبی فایلهای Resource و کلاسهای متناظر با آنهاست که براساس استانداردها باید برطبق مسیر فیزیکی آنها در پروژه باشد مثل Global.Texts یا Views.LogOn. همچنین در متد RestoreResourceFiles نمونه هایی از مسیرهای این فایلها آورده شده است.
پس از اجرای متد Build از کلاس BuildManager، یعنی پس از build پروژه Resource در زمان اجرا، باید ابتدا فایلهای تولیدی به مسیرهای مربوطه در فولدر bin برنامه کپی شده سپس فولدرهای تولیدشده توسط msbuild، حذف شوند. این کار در متد CopyDlls از کلاسResXResourceFileManager انجام میشود. هرچند در این قسمت فرض شده است که فایل csprj. موجود برای حالت debug تنظیم شده است.
نکته: دقت کنید که در این قسمت بلافاصله پس از کپی فایلها در مقصد با توجه به توضیحات ابتدای این مطلب سایت Restart خواهد شد که یکی از ضعفهای عمده این روش به شمار میرود.
سایر متدهای موجود نیز برای برگرداندن تنظیمات اولیه بکار میروند. در این متدها از محتویات فولدر Defaults استفاده میشود.
نکته: درصورت ساخت دوباره اسمبلی و یا بازگرداندن اسمبلیهای اولیه، از آنجاکه وبسایت Restart خواهد شد، بنابراین بهتر است تا صفحه جاری بلافاصله پس از اتمام عملیات،دوباره بارگذاری شود. مثلا اگر از ajax برای اعمال این دستورات استفاده شده باشد میتوان با استفاده از کدی مشابه زیر در پایان فرایند صفحه را دوباره بارگذاری کرد:
window.location.reload();
در قسمت بعدی راه حل بهتری با استفاده از فراهم کردن پرووایدر سفارشی برای مدیریت فایلهای Resource ارائه میشود.
در مقاله قبلی در مورد تعدادی از Layoutها صحبت کردیم و در این بخش به ادامهی آن پرداخته و دو مبحث GridPanel و Custom Layout را بررسی میکنیم.
GridPanel
پنل پیش فرضی است که موقع ایجاد یک پروژه جدید WPF ایجاد میشود. چیدمان این نوع پنل به صورت سطر و ستون است و کارکرد آن بسیار مشابه جداول در HTML میباشد؛ با این تفاوت که در اینجا انعطاف پذیری بیشتری وجود دارد. هر سلول میتواند شامل چندین کنترل شود و یا هر کنترل میتواند چندین سلول را به خود احتصاص دهند و حتی میتواند روی کنترلهای دیگر قرار بگیرند و همپوشانی کنترلها را داشته باشیم.
تگ Grid Panel شامل دو تگ برای تعریف سطرها و ستونها میباشد با استفاده
از تگ Row Definition و Column Definition به تعیین تعداد سطر و ستونها و
اندازه آنها میپردازیم:
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="28" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="200" /> </Grid.ColumnDefinitions> </Grid>
گرید پنل بالا شامل 4 سطر
و دو ستون است و تعیین اندازه آنها توسط دو خاصیت Width و Height مشخص
شده است که نحوه مقداردهی آنها به صورت زیر است:
Fixed : یک مقدار ثابت، مثل سطر آخری که در کد بالا قرار میگیرد. این مقدار بر اساس یک واحد منطقی است و نه پیکسل که در این مقاله قبلا بررسی کردهایم.
Auto : به مقداری که احتیاج دارد فضایی را بخود اختصاص میدهد.
* : هر آنچه از فضای موجود باقی مانده است را به خود اختصاص میدهد. علامت ستاره یک واحد نسبی است؛ به این صورت که میتوانید مقدار فضا را به صورت زیر نیز بیان کنید.*3 و *2 به این معنی است که از پنج قسمت فضای باقیمانده سه قسمت و بعدی دو قسمت را به خود اختصاص میدهد. عبارت * با *1 برابر است. عموما با این علامت فضا را به شکل درصد بیان میکنند:
Fixed : یک مقدار ثابت، مثل سطر آخری که در کد بالا قرار میگیرد. این مقدار بر اساس یک واحد منطقی است و نه پیکسل که در این مقاله قبلا بررسی کردهایم.
Auto : به مقداری که احتیاج دارد فضایی را بخود اختصاص میدهد.
* : هر آنچه از فضای موجود باقی مانده است را به خود اختصاص میدهد. علامت ستاره یک واحد نسبی است؛ به این صورت که میتوانید مقدار فضا را به صورت زیر نیز بیان کنید.*3 و *2 به این معنی است که از پنج قسمت فضای باقیمانده سه قسمت و بعدی دو قسمت را به خود اختصاص میدهد. عبارت * با *1 برابر است. عموما با این علامت فضا را به شکل درصد بیان میکنند:
<ColumnDefinition Width="69*" /> <!-- Take 69% of remainder --> <ColumnDefinition Width="31*"/> <!-- Take 31% of remainder -->
نحوهی اضافه کردنالمانها به گرید به صورت زیر پس از تعیین تعداد سطرها و
ستونها انجام میگیرد و جایگاه هر المان در ستون یا سطر مربوطه توسط یک
attached Dependency Property به نامهای Grid.Column یا Grid.Row صورت
میگیرد. خصوصیات Horizontal alignment و vertical Alignment هم برای تعیین
موقعیت قرار گیری اشیاء در سلول به کار میروند و فاصلهی آنها (کنترل ها) از
لبههای گرید با margin محاسبه میشود.
<Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="28" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="200" /> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Content="Name:"/> <Label Grid.Row="1" Grid.Column="0" Content="E-Mail:"/> <Label Grid.Row="2" Grid.Column="0" Content="Comment:"/> <TextBox Grid.Column="1" Grid.Row="0" Margin="3" /> <TextBox Grid.Column="1" Grid.Row="1" Margin="3" /> <TextBox Grid.Column="1" Grid.Row="2" Margin="3" /> <Button Grid.Column="1" Grid.Row="3" HorizontalAlignment="Right" MinWidth="80" Margin="3" Content="Send" /> </Grid>
تغییر اندازه در سمت کد هم میتواند توسط کدهای صورت گیرد.
Auto sized GridLength.Auto Star sized new GridLength(1,GridUnitType.Star) Fixed size new GridLength(100,GridUnitType.Pixel)
Grid grid = new Grid(); ColumnDefinition col1 = new ColumnDefinition(); col1.Width = GridLength.Auto; ColumnDefinition col2 = new ColumnDefinition(); col2.Width = new GridLength(1,GridUnitType.Star); grid.ColumnDefinitions.Add(col1); grid.ColumnDefinitions.Add(col2);
قابلیت تغییر اندازهی سطر و ستون توسط کاربر
یکی از تگهای ویژه داخل گری،د تگ Grid Splitter است. برای قرارگیری تگ splitter ابتدا باید یک سطر یا ستون بین سطر و ستون هایی که میخواهید از یکدیگر جدا شوند ایجاد کنید و اندازهی آن را auto تعیین کنید و سپس مانند بقیهی اشیا توسط Grid.Column یا Grid.Row مانند کد زیر تگ splitter را به آن اختصاص دهید.
یکی از تگهای ویژه داخل گری،د تگ Grid Splitter است. برای قرارگیری تگ splitter ابتدا باید یک سطر یا ستون بین سطر و ستون هایی که میخواهید از یکدیگر جدا شوند ایجاد کنید و اندازهی آن را auto تعیین کنید و سپس مانند بقیهی اشیا توسط Grid.Column یا Grid.Row مانند کد زیر تگ splitter را به آن اختصاص دهید.
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Content="Left" Grid.Column="0" /> <GridSplitter HorizontalAlignment="Right" VerticalAlignment="Stretch" Grid.Column="1" ResizeBehavior="PreviousAndNext" Width="5" Background="#FFBCBCBC"/> <Label Content="Right" Grid.Column="2" /> </Grid>
BasedOnAlignment | مقدار پیش فرض این گزینه است و مشخص میکند سطر یا ستونی طرفی باید تغییر اندازه دهد که در Alignment آن آمده است |
CurrentAndNext | ستون یا سطر جاری به همراه ستون یا سطر بعدی |
PreviousAndCurrent | ستون یا سطر جاری به همراه ستون یا سطر قبلی |
PreviousAndNext | سطر یا ستون قبلی و بعدی که بهترین گزینه برای انتخاب است. |
خاصیت ResizeDirection جهت تغییر اندازه را مشخص میکند که شامل سه مقدار
Row,Column و Auto است که مقدار پیش فرض آن auto است و نیازی به ذکر آن
نیست و خود سیستم میداند که باید تغییر اندازه در چه جهتی صورت بگیرد.
ساخت Custom Layout یا یک پنل سفارشی (اختصاصی)
در این دو قسمت، شما با پنلهای متفاوتی آشنا شدید که قابلیتهای مفیدی داشتند؛ ولی گاهی اوقات هیچ کدام از اینها به کار شما نمیآیند و دوست دارید پنلی داشته باشید که مطابق میل شما عمل کند. برای ساخت یک پنل سفارشی یک کلاس میسازیم که از کلاس Panel ارث بری میکند. در اینجا دو متد برای Override کردن وجود دارند:
MeasureOverride : تعیین اندازه پنل بر اساس اندازه تعیین شده برای المانهای فرزند و فضای موجود.
ArrangeOverride: مرتب سازی المانها در فضای موجود نهایی.
کد نمونه:
لینکهای زیر تعدادی از پنلهای سفارشی پر طرفدار هستند که بر روی اینترنت به اشتراک گذاشته شده اند:
TreeMapPanel
Animating Tile Panel
Radial Panel
Element Flow Panel
Ribbon Panel
خواصی که باید در Layoutها با آنها بیشتر آشنا شویم:
Horizontal & Vertical Alignment
با دادن این خاصیت به کنترلهای موجود، نحوه قرار گیری و موقعیت آنها مشخص میگردد. جدول زیر بر ساس انواع موقعیتهای مختلف تشکیل شده است:
این خاصیتها حتما برای شما آشنا هستند. خاصیت margin فاصله کنترل از لبههای Layout است و خاصیت Padding فاصله محتویات کنترل از لبههای کنترل است.
Clipping
در صورتی که خاصیت ClipToBounds پنل برابر False باشند به این معناست که المانها میتوانند از لبههای پنل خارج شوند، در صورتی که برابر True باشد مقدار خارج شده نمایش نمییابد.
موقعیکه از پنلی استفاده میکنید که با تمام شدن ناحیهاش روبرو شدهاید ولی کنترلهای داخلش هنوز ادامه دارند، نیاز به یک اسکرول به شدت احساس میشود. در این حالت میتوان از ScrollViewer استفاده کرد.
ساخت Custom Layout یا یک پنل سفارشی (اختصاصی)
در این دو قسمت، شما با پنلهای متفاوتی آشنا شدید که قابلیتهای مفیدی داشتند؛ ولی گاهی اوقات هیچ کدام از اینها به کار شما نمیآیند و دوست دارید پنلی داشته باشید که مطابق میل شما عمل کند. برای ساخت یک پنل سفارشی یک کلاس میسازیم که از کلاس Panel ارث بری میکند. در اینجا دو متد برای Override کردن وجود دارند:
MeasureOverride : تعیین اندازه پنل بر اساس اندازه تعیین شده برای المانهای فرزند و فضای موجود.
ArrangeOverride: مرتب سازی المانها در فضای موجود نهایی.
کد نمونه:
public class MySimplePanel : Panel { // Make the panel as big as the biggest element protected override Size MeasureOverride(Size availableSize) { Size maxSize = new Size(); foreach( UIElement child in InternalChildern) { child.Measure( availableSize ); maxSize.Height = Math.Max( child.DesiredSize.Height, maxSize.Height); maxSize.Width= Math.Max( child.DesiredSize.Width, maxSize.Width); } } // Arrange the child elements to their final position protected override Size ArrangeOverride(Size finalSize) { foreach( UIElement child in InternalChildern) { child.Arrange( new Rect( finalSize ) ); } } }
TreeMapPanel
Animating Tile Panel
Radial Panel
Element Flow Panel
Ribbon Panel
خواصی که باید در Layoutها با آنها بیشتر آشنا شویم:
Horizontal & Vertical Alignment
با دادن این خاصیت به کنترلهای موجود، نحوه قرار گیری و موقعیت آنها مشخص میگردد. جدول زیر بر ساس انواع موقعیتهای مختلف تشکیل شده است:
Margin & Padding
این خاصیتها حتما برای شما آشنا هستند. خاصیت margin فاصله کنترل از لبههای Layout است و خاصیت Padding فاصله محتویات کنترل از لبههای کنترل است.
Clipping
در صورتی که خاصیت ClipToBounds پنل برابر False باشند به این معناست که المانها میتوانند از لبههای پنل خارج شوند، در صورتی که برابر True باشد مقدار خارج شده نمایش نمییابد.
Scrolling
موقعیکه از پنلی استفاده میکنید که با تمام شدن ناحیهاش روبرو شدهاید ولی کنترلهای داخلش هنوز ادامه دارند، نیاز به یک اسکرول به شدت احساس میشود. در این حالت میتوان از ScrollViewer استفاده کرد.
<ScrollViewer> <StackPanel> <Button Content="First Item" /> <Button Content="Second Item" /> <Button Content="Third Item" /> </StackPanel> </ScrollViewer>
نظرات مطالب
فارسی نویسی و iTextSharp
با سلام ، من کد زیر رو نوشتم
آیا امکانش هست که ما به جای اینکه بیایم در هر Cell کد زیر را بنویسیم یک بار برای کد جدول اینو تعریف کنیم؟
چون اگه بخواهیم کدهای مربوط به تنظیم فوت رو برای هر Cell بنویسیم یه خورده کدها زیاد میشه.
ممنون میشم راهنمایی کنید
مرسی
var m = new ITModel.ITModelContainer(); var list = (from pp in m.PERSONNELs select pp).ToList(); string pdfpath = Server.MapPath("PDFs"); using (var pdfDoc = new Document(PageSize.A4)) { var pdfWriter = PdfWriter.GetInstance(pdfDoc, new FileStream(pdfpath + "/Personnel2.pdf", FileMode.Create)); pdfDoc.Open(); var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf"; var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); var tahomaFont = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK); float[] widths = new float[] { 1f, 2f }; PdfPTable table = new PdfPTable(2) { TotalWidth = 216f, LockedWidth = true, HorizontalAlignment = 0, SpacingBefore = 20f, SpacingAfter = 30f }; table.SetWidths(widths); PdfPCell cell = new PdfPCell(new Phrase("لیست پرسنل", tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, Colspan = 2, Border = 0, HorizontalAlignment = 1 }; table.AddCell(cell); foreach (var item in list) { PdfPCell cell2 = new PdfPCell(new Phrase(item.PERSON_ID.ToString(), tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; PdfPCell cell3 = new PdfPCell(new Phrase(item.FIRST_NAME + " " + item.LAST_NAME, tahomaFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; table.AddCell(cell2); table.AddCell(cell3); } pdfDoc.Add(table); }
{ RunDirection = PdfWriter.RUN_DIRECTION_RTL };
ممنون میشم راهنمایی کنید
مرسی
برای قرار دادن عکسی داخل یک سلول جدول، نیازی نیست سایز و scale آنرا تغییر دهید؛ چون خودش گزینهی fit دارد:
var pdfCell = new PdfPCell(Image.GetInstance(imageFilePath), fit: true);