Check | Code | Description |
Is Null | if(variable is null) return true; |
|
Is Not Null | if(variable is { }) return false |
|
Is Not Null | if(variable is object) return false |
|
Is Null | if(variable == null) return true |
|
Is Not Null | if(variable != null) return false |
|
- لیستی از حقوق کارکنان را داریم. در گزارش نهایی آن نیاز است عدد حقوق کارکنانی با مبلغ کمتر از 1000، با رنگی دیگر نمایش داده شوند.
همچنین در این گزارش هر ردیفی که در ماه 7 واقع شده نیز ظاهر عدد سلول مربوط به آن ماه، به رنگ قهوهای و زمینه زرد تغییر یابد.
- در ستون مشخصات افراد این گزارش، نیاز است تصویر کارمند به همراه نام او در ذیل این تصویر (داخل یک سلول) نمایش داده شوند.
چیزی شبیه به این گزارش!
مورد اول در گزارشات، اصطلاحا به conditional formatting معروف است و مورد دوم مرتبط است به تهیه قالبهای سفارشی، بجای استفاده از قالبهای سلولهای پیش فرض PdfReport؛ که در ادامه نحوه انجام این موارد را بررسی خواهیم کرد.
ابتدا سورس کامل این مثال را ملاحظه نمائید:
using System; using iTextSharp.text; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; using PdfRpt.FluentInterface; namespace PdfReportSamples.CustomCellTemplate { public class CustomCellTemplatePdfReport { public IPdfReportData CreatePdfReport() { return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.RightToLeft); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" }); doc.Compression(new CompressionSettings { CompressionLevel = CompressionLevel.BestCompression, EnableCompression = true }); }) .DefaultFonts(fonts => { fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("گزارش جدید ما"); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.SnowyPineTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.MultipleColumnsPerPage(new MultipleColumnsPerPage { ColumnsGap = 20, ColumnsPerPage = 2, ColumnsWidth = 250, IsRightToLeft = true, TopMargin = 7 }); }) .MainTableDataSource(dataSource => { var table = new System.Data.DataTable("لیست حقوق"); table.Columns.Add("شخص", typeof(string)); table.Columns.Add("ماه", typeof(int)); table.Columns.Add("مبلغ", typeof(decimal)); var rnd = new Random(); for (int i = 0; i < 200; i++) table.Rows.Add("شخص " + i, rnd.Next(1, 12), rnd.Next(400, 2000)); dataSource.DataTable(table); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); events.CellCreated(args => { //change the background color of the cell based on the value if (args.RowType == RowType.DataTableRow && args.Cell.RowData.Value != null && args.Cell.RowData.Value is decimal) { if ((decimal)args.Cell.RowData.Value <= 1000) args.Cell.BasicProperties.BackgroundColor = BaseColor.CYAN; } }); }) .MainTableSummarySettings(summary => { summary.OverallSummarySettings("جمع کل"); summary.PageSummarySettings("جمع صفحه"); summary.PreviousPageSummarySettings("نقل از ستون قبل"); }) .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("شخص"); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(3); column.HeaderCell("شخص"); column.ColumnItemsTemplate(t => t.CustomTemplate(new MyCustomCellTemplate())); }); columns.AddColumn(column => { column.PropertyName("ماه"); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(2); column.HeaderCell("ماه"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.ConditionalFormatFormula(list => { var cellValue = int.Parse(list.GetSafeStringValueOf("ماه", nullValue: "0")); if (cellValue == 7) { return new CellBasicProperties { PdfFontStyle = DocumentFontStyle.Bold | DocumentFontStyle.Underline, FontColor = new BaseColor(System.Drawing.Color.Brown), BackgroundColor = new BaseColor(System.Drawing.Color.Yellow) }; } return new CellBasicProperties { PdfFontStyle = DocumentFontStyle.Normal }; }); }); }); columns.AddColumn(column => { column.PropertyName("مبلغ"); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(2); column.HeaderCell("مبلغ"); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); }); }) .Export(export => { export.ToXml(); export.ToExcel(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptDataTableSample.pdf")); } } }
using System; using System.Collections.Generic; using iTextSharp.text; using iTextSharp.text.pdf; using PdfRpt.Core.Contracts; using PdfRpt.Core.Helper; namespace PdfReportSamples.CustomCellTemplate { public class MyCustomCellTemplate : IColumnItemsTemplate { Random _rnd = new Random(); public void CellRendered(PdfPCell cell, Rectangle position, PdfContentByte[] canvases, CellAttributes attributes) { } public CellBasicProperties BasicProperties { set; get; } public Func<IList<CellData>, CellBasicProperties> ConditionalFormatFormula { set; get; } public PdfPCell RenderingCell(CellAttributes attributes) { var pdfCell = new PdfPCell(); var table = new PdfPTable(1) { RunDirection = PdfWriter.RUN_DIRECTION_RTL }; var filePath = AppPath.ApplicationPath + "\\Images\\" + _rnd.Next(1, 5).ToString("00") + ".png"; var photo = PdfImageHelper.GetITextSharpImageFromImageFile(filePath); table.AddCell(new PdfPCell(photo, fit: false) { Border = 0, VerticalAlignment = Element.ALIGN_BOTTOM, HorizontalAlignment = Element.ALIGN_CENTER }); var name = attributes.RowData.TableRowData.GetSafeStringValueOf("شخص"); table.AddCell(new PdfPCell(attributes.BasicProperties.PdfFont.FontSelector.Process(name)) { Border = 0, HorizontalAlignment = Element.ALIGN_CENTER }); pdfCell.AddElement(table); return pdfCell; } } }
توضیحات:
- در این مثال از منبع دادهای از نوع DataTable استفاده شده است؛ که نحوه بکارگیری آنرا در متد MainTableDataSource ملاحظه میکنید. ستونهای تعریف شده در MainTableColumns نیز بر اساس ستونهای DataTable مشخص شدهاند.
- در متد DocumentPreferences، نحوه مشخص سازی فشرده سازی نهایی فایل PDF را ملاحظه میکنید. این مورد از مزایای استفاده از فایلهای PDF است.
- برای اعمال فرمت شرطی اطلاعات در PdfReport دو روش وجود دارد.
الف) استفاده از متد MainTableEvents و کار کردن با رخدادهای تعریف شده در آن مانند CellCreated. در اینجا میتوان در نحوه رندر شدن یک سلول دخالت کرد:
events.CellCreated(args => { //change the background color of the cell based on the value if (args.RowType == RowType.DataTableRow && args.Cell.RowData.Value != null && args.Cell.RowData.Value is decimal) { if ((decimal)args.Cell.RowData.Value <= 1000) args.Cell.BasicProperties.BackgroundColor = BaseColor.CYAN; } });
ب) همانطور که در قسمت تعریف ستون «ماه» ملاحظه میکنید، توسط متد template.ConditionalFormatFormula نیز، امکان فرمت شرطی اطلاعات فراهم شده است. در اینجا میتوان به لیست اطلاعات سلولهای ردیف جاری دسترسی یافت و سپس بر اساس آن تصمیم گیری کرد.
- جهت تعریف قالبهای سفارشی سلولها کافی است اینترفیس IColumnItemsTemplate را پیاده سازی کنیم؛ که نمونهای از آن را در کدهای MyCustomCellTemplate فوق ملاحظه میکنید. در اینجا فرصت خواهید داشت هر شکل و طرح متنوعی را تهیه کرده و به صورت یک PdfPCell بازگشت دهید. برای نمونه در مثال فوق، یک جدول را در سلول تعریف شده قرار دادهایم. این جدول یک ستون دارد و هر سلولی که به آن اضافه خواهد شد، یک ردیف را تشکیل خواهد داد. در ردیف اول آن تصویر قرار گرفته و در ردیف دوم آن مقدار سلول جاری.
یافتن شماره سند متناظر لوسین
همان مثال «استفاده از لوسین برای برجسته سازی عبارت جستجو شده در نتایج حاصل» را در نظر بگیرید. در ابتدا نیاز است شماره یک مطلب را تبدیل به شماره سند لوسین کنیم. برای مثال ممکن است Id یک مطلب 1000 باشد، اما شماره سند متناظر آن در لوسین 800 ثبت شده باشد. بنابراین جستجوی ذیل الزامی است:
static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_29; static readonly IndexSearcher _searcher = new IndexSearcher(@"c:\path\idx", readOnly: true); private static int GetLuceneDocumentNumber(int postId) { var analyzer = new StandardAnalyzer(_version); var parser = new QueryParser(_version, "Id", analyzer); var query = parser.Parse(postId.ToString()); var doc = _searcher.Search(query, 1); if (doc.totalHits == 0) { return 0; } return doc.scoreDocs[0].doc; }
در اینجا بر اساس شماره یک مطلب، کوئری متناظر با آن تشکیل شده و جستجویی بر روی اسناد ثبت شده در ایندکسهای لوسین صورت میگیرد. اگر اطلاعاتی یافت شد، شماره سند متناظر بازگشت داده میشود.
از این جهت به شماره سند یاد شده نیاز داریم که قرار است مطالب مرتبط با کل این سند را بیابیم.
ساختن کوئریهای MoreLikeThis
امکانات یافتن مطالب مشابه یک مطلب در اسمبلی Lucene.Net.Contrib.Queries.dll قرار دارد. بنابراین در اینجا نیاز به فایلهای پروژه Lucene.Net Contrib وجود دارد.
پس از یافتن شماره سند متناظر با یک مطلب، اکنون نوبت به ساخت کوئریهای پیشرفته MoreLikeThis است که نحوه انجام تنظیمات آنرا در ذیل مشاهده میکنید:
private static Query CreateMoreLikeThisQuery(int postId) { var docNum = GetLuceneDocumentNumber(postId); if (docNum == 0) return null; var analyzer = new StandardAnalyzer(_version); var reader = _searcher.GetIndexReader(); var moreLikeThis = new MoreLikeThis(reader); moreLikeThis.SetAnalyzer(analyzer); moreLikeThis.SetFieldNames(new[] { "Title", "Body"}); moreLikeThis.SetMinDocFreq(1); moreLikeThis.SetMinTermFreq(1); moreLikeThis.SetBoost(true); return moreLikeThis.Like(docNum); }
در اینجا فیلدهایی که قرار است در جستجو حضور داشته باشند توسط متد SetFieldNames معرفی میشوند. توسط متد SetMinDocFreq مشخص میکنیم که واژههای مشابه و مرتبط باید حداقل در چند سند ظاهر شده باشند. همچنین توسط متد SetMinTermFreq تعیین میگردد که یک واژه باید چندبار در این اسناد وجود داشته باشد. متد SetBoost سبب میشود که آنالیز بهتری بر اساس رتبه بندیهای حاصل صورت گیرد.
نمایش مطالب مرتبط توسط کوئری MoreLikeThis
پس از این تنظیمات، متد moreLikeThis.Like، یک شیء Query را در اختیار ما قرار خواهد داد. از اینجای کار به بعد همانند سایر مطالب مشابه است. بر اساس این کوئری، جستجویی صورت گرفته و سپس اطلاعات یافت شده نمایش داده میشود:
private static void ShowMoreLikeThisPostItems(int postId) { var query = CreateMoreLikeThisQuery(postId); if (query == null) return; var hits = _searcher.Search(query, n: 10); foreach (var item in hits.scoreDocs) { var doc = _searcher.Doc(item.doc); var id = doc.Get("Id"); var title = doc.Get("Title"); Console.WriteLine(title); } }
.jqGrid('navButtonAdd', '#pager', { caption: "تنظیم نمایش ستونها", title: "Reorder Columns", onClickButton: function () { jQuery("#list").jqGrid('columnChooser', { done: function (perm) { if (perm) { $("#list").jqGrid("remapColumns", perm, true, false); } var colModel = $("#list").jqGrid('getGridParam', 'colModel'); var hiddenColumns = new Array(); for (var i = 0; i < colModel.length; i++) { if (colModel[i].hidden) { hiddenColumns.push(colModel[i].name); } } $.ajax({ type: "POST", url: "@Url.Action("HiddenColumns","Home")", dataType: "json", traditional: true, data: { hiddenColumns: hiddenColumns } }); } }); } })
var colModel = $("#list").jqGrid('getGridParam', 'colModel');
public ActionResult HiddenColumns(string[] hiddenColumns) { //todo: save it in the DB or cookies or session .... return Content("OK"); }
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); }
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return toEnglishNumbers(data) .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); } private static string toEnglishNumbers(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data.Replace("\u0660", "0") //٠ .Replace("\u06F0", "0") //۰ .Replace("\u0661", "1") //١ .Replace("\u06F1", "1") //۱ .Replace("\u0662", "2") //٢ .Replace("\u06F2", "2") //۲ .Replace("\u0663", "3") //٣ .Replace("\u06F3", "3") //۳ .Replace("\u0664", "4") //٤ .Replace("\u06F4", "4") //۴ .Replace("\u0665", "5") //٥ .Replace("\u06F5", "5") //۵ .Replace("\u0666", "6") //٦ .Replace("\u06F6", "6") //۶ .Replace("\u0667", "7") //٧ .Replace("\u06F7", "7") //۷ .Replace("\u0668", "8") //٨ .Replace("\u06F8", "8") //۸ .Replace("\u0669", "9") //٩ .Replace("\u06F9", "9"); //۹ }
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
.Replace("\u0669", "9") //٩
.Replace('\u0669', '9') //٩
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
private static string toPersianNumbersUsingStringBuilder(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var strBuilder = new StringBuilder(data); for (var i = 0; i < strBuilder.Length; i++) { switch (strBuilder[i]) { case '0': case '\u0660': strBuilder[i] = '\u06F0'; break; case '1': case '\u0661': strBuilder[i] = '\u06F1'; break; case '2': case '\u0662': strBuilder[i] = '\u06F2'; break; case '3': case '\u0663': strBuilder[i] = '\u06F3'; break; case '4': case '\u0664': strBuilder[i] = '\u06F4'; break; case '5': case '\u0665': strBuilder[i] = '\u06F5'; break; case '6': case '\u0666': strBuilder[i] = '\u06F6'; break; case '7': case '\u0667': strBuilder[i] = '\u06F7'; break; case '8': case '\u0668': strBuilder[i] = '\u06F8'; break; case '9': case '\u0669': strBuilder[i] = '\u06F9'; break; default: strBuilder[i] = strBuilder[i]; break; } } return strBuilder.ToString(); }
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
private static string toPersianNumbersUsingToCharArray(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var letters = data.ToCharArray(); for (var i = 0; i < letters.Length; i++) { switch (letters[i]) { case '0': case '\u0660': letters[i] = '\u06F0'; break; // مانند قبل } } return new string(letters); }
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
private static string toPersianNumbersUsingStringCreate(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return string.Create(data.Length, data, (chars, context) => { for (var i = 0; i < data.Length; i++) { switch (context[i]) { case '0': case '\u0660': chars[i] = '\u06F0'; break; // مانند قبل } } }); }
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
return string.Create(data.Length, data, (chars, context) => {});
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.
مثالی از using declarations
تا پیش از C# 8.0، روش متداول کار با عبارات using به صورت زیر است و به آن استفاده از using statements گفته میشود:
class Program { static void UsingOld() { using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { var s = reader.ReadToEnd(); // Do something with data } }
اکنون در C# 8.0 میتوان قطعه کد فوق را به کمک using declarations به صورت زیر خلاصه کرد:
class Program { static void UsingNew(string[] args) { using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); var s = reader.ReadToEnd(); // Do something with data }
میدان دید using declarations
پس از این تغییرات، سؤال مهمی که مطرح میشود این است: متغیرهایی که توسط using declaration تعریف میشوند، تا چه زمانی زنده نگه داشته میشوند. به عبارتی متد UsingOldScope آیا همانند متد UsingNewScope عمل میکند؟ آیا متغیر buffer آن همانند متد UsingOldScope خارج از میدان دید usingها قرار میگیرد؟
class Program { static void UsingNewScope() { string buffer = null; using Stream file = new FileStream("input.txt", FileMode.Open); using StreamReader reader = new StreamReader(file); buffer = reader.ReadToEnd(); // Do something with data buffer = null; } static void UsingOldScope(string[] args) { string buffer = null; using (var file = new FileStream("input.txt", FileMode.Open)) using (var reader = new StreamReader(file)) { buffer = reader.ReadToEnd(); } // Do something with data buffer = null; }
اما زمانیکه از using declarations استفاده میشود (مانند متد UsingNewScope)، دیگر این {} را نداریم. اینبار scope تعریف شده، تا «پایان متد» ادامه پیدا میکند و سپس متد Dispose اشیاء ارجاعی، فراخوانی میگردد. بدیهی است در اینجا نیز همانند قبل، همان قطعه کد try/finally توسط کامپایلر جهت فراخوانی متد Dispose، تشکیل خواهد شد. بنابراین اگر بخواهیم متد UsingNewScope را توسط using statements پیشین بازنویسی کنیم، به یک چنین قطعه کدی خواهیم رسید که scope پس از using declarations، تا آخر متد ادامه پیدا میکند:
string buffer = null; using (var file = new FileStream("input.txt", FileMode.Open)) { using (var reader = new StreamReader(file)) { buffer = reader.ReadToEnd(); buffer = null; } }
پاسخ: بله. میتوان با تعریف یک {}، میدان دید متغیرهای ارجاعی توسط using declarations را محدود کرد:
private static void UsingDeclarationWithScope() { { using var r1 = new AResource(); r1.UseIt(); } // r1 is disposed here! Console.WriteLine("r1 is already disposed"); }
سؤال: آیا using declarations تمام قابلیتهای using statements را ارائه میدهند؟
پاسخ: خیر. فرض کنید کلاس AResource از نوع IDisposable تعریف شدهاست:
public class AResource : IDisposable { public void UseIt() => Console.WriteLine(nameof(UseIt)); public void Dispose() => Console.WriteLine($"Dispose {nameof(AResource)}"); }
class Program { public static AResource GetTheResource() => new AResource();
using (GetTheResource()) { // do something here } // resource is disposed here
using GetTheResource(); // Compiler error
using var _ = GetTheResource(); // Works fine
Long Polling در WCF
در این حالت thread جاری سمت کلاینت نیز در حالت wait است و برنامه سمت کلاینت از کار میافتد تا زمانی که پاسخ از سرور دریافت نماید. اما در WCF به صورت پیش فرض هر درخواست ارسالی باید در طی یک دقیقه در اختیار سرور قرار گیرد و سرور نیز باید در طی یک دقیقه پاسخ مورد نظر را برگرداند(مقادیر خواص SendTimeout و ReceiveTimeout برای مدیریت این موارد به کار میروند). افزایش مقادیر این خواص کمک خاصی به این حالت نمیکند زیرا هم چنان کلاینت در حالت wait است و سرور نیز پاسخ خاصی ارسال نمیکند. حتی اگر کل عملیات را به صورت Async پیاده سازی نماییم باز ممکن است بعد از منقضی شدن زمان پردازش با یک TimeoutException برنامه از کار بیفتد. برای حل اینگونه موارد پیاده سازی سرویسها به صورت Long Polling به ما کمک خوبی خواهد کرد.
حال سناریو زیر را در نظر بگیرید:
سمت سرور:
»یک درخواست دریافت میشود؛
»سرور در حالت wait (البته توسط یک thread دیگر) منتظر تامین منابع برای پاسخ به کلاینت است؛
»در نهایت پاسخ مورد نظر ارسال خواهد شد.
سمت کلاینت:
»درخواست مورد نظر به سرور ارسال میشود؛
»کلاینت منتظر پاسخ از سمت سرور است(البته توسط یک Thread دیگر)؛
»اگر در حین انتظار برای پاسخ از سمت سرور، با یک TimeoutException روبرو شدیم به جای توقف برنامه و نمایش پیغام خطای Server is not available، باید عملیات به صورت خودکار restart شود.
»در نهایت پاسخ مورد نظر دریافت خواهد شد.
پیاده سازی این سناریو در WCF کار پیچیده ای نیست. بدین منظور میتوانید از کلاس زیر استفاده کنید( لینک دانلود ). سورس آن به صورت زیر است:
public abstract class LongPollingAsyncResult<TResult> : IAsyncResult where TResult : class { #region - Fields - private AsyncCallback _callback; private TimeSpan _timoutSpan; private TimeSpan _intervalWaitSpan; #endregion #region - Properties - public Exception Exception { get; private set; } public TResult Result { get; private set; } public object SyncRoot { get; private set; } #endregion #region - Ctor - public LongPollingAsyncResult(AsyncCallback callback, object asyncState, int timeoutSeconds = 300, int intervalWaitMilliseconds = 500) { SyncRoot = new object(); _callback = callback; AsyncState = asyncState; AsyncWaitHandle = new ManualResetEvent(IsCompleted); _timoutSpan = TimeSpan.FromSeconds(timeoutSeconds); _intervalWaitSpan = TimeSpan.FromMilliseconds(intervalWaitMilliseconds); ThreadPool.QueueUserWorkItem(new WaitCallback(LoopWithIntervalAndTimeout)); } #endregion #region - Private Helper Methods - private void LoopWithIntervalAndTimeout(object input) { try { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); while (!IsCompleted) { if (stopwatch.Elapsed > _timoutSpan) throw new TimeoutException(); DoWork(); if (!IsCompleted) Thread.Sleep(_intervalWaitSpan); } } catch (Exception e) { Complete(null, e); } } #endregion #region - Protected/Abstract Methods - protected void Complete(TResult result, Exception e = null, bool completedSynchronously = false) { lock (SyncRoot) { CompletedSynchronously = completedSynchronously; Result = result; Exception = e; IsCompleted = true; if (_callback != null) _callback(this); } } protected abstract void DoWork(); #endregion #region - Public Methods - public TResult WaitForResult() { if (!IsCompleted) AsyncWaitHandle.WaitOne(); if (Exception != null) { if (Exception is TimeoutException && WebOperationContext.Current != null) WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.RequestTimeout; throw Exception; } return Result; } #endregion #region - IAsyncResult Implementation - public object AsyncState { get; private set; } public WaitHandle AsyncWaitHandle { get; private set; } public bool CompletedSynchronously { get; private set; } public bool IsCompleted { get; private set; } #endregion }
برای استفاده از کلاس تهیه شده ابتدا باید عملیات خود را به صورت Async پیاده سازی نمایید که در این مقاله به صورت کامل شرح داده شده است.
یک مثال
قصد داریم Operation زیر را به صورت Long Polling پیاده سازی نماییم:
[OperationContract] public string GetNotification();
[OperationContract(AsyncPattern = true)] public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state); public string EndWaitNotification(IAsyncResult result);
public class MyNotificationResult : LongPollingAsyncResult<string> { protected override DoWork() { کدهای مورد نظر را اینجا قرار دهید base.Complete(...) } }
public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state) { return new MyNotificationResult(callback, state); } public string EndWaitNotification(IAsyncResult result) { MyNotificationResult myResult = result as MyNotificationResult; if(myResult == null) throw new ArgumentException("result was of the wrong type!"); myResult.WaitForResult(); return myResult.Result; }
معرفی پروژه NotifyPropertyWeaver
اگر به Wiki آن مراجعه نمائید، لیست افزونههای قابل توجهی را در مورد آن خواهید یافت که PropertyChanged تنها یکی از آنها است.
پیشنیازها
الف) صفحه پروژه در GitHub
ب) دریافت از طریق نوگت
روش استفاده
پس از نصب بسته نوگت پروژه PropertyChanged.Fody
PM> Install-Package PropertyChanged.Fody
using PropertyChanged; namespace AOP02 { [ImplementPropertyChanged] public class Person { public string Id { set; get; } public string Name { set; get; } } }
------ Build started: Project: AOP02, Configuration: Debug x86 ------ Fody (version 1.13.6.1) Executing Finished Fody 287ms. AOP02 -> D:\Prog\AOP02\bin\Debug\AOP02.exe ========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
using System; using System.ComponentModel; using System.Runtime.CompilerServices; namespace AOP02 { public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public string Id { [System.Runtime.CompilerServices.CompilerGenerated] get { return this.<Id>k__BackingField; } [System.Runtime.CompilerServices.CompilerGenerated] set { if (string.Equals(this.<Id>k__BackingField, value, System.StringComparison.Ordinal)) { return; } this.<Id>k__BackingField = value; this.OnPropertyChanged("Id"); } } public string Name { [System.Runtime.CompilerServices.CompilerGenerated] get { return this.<Name>k__BackingField; } [System.Runtime.CompilerServices.CompilerGenerated] set { if (string.Equals(this.<Name>k__BackingField, value, System.StringComparison.Ordinal)) { return; } this.<Name>k__BackingField = value; this.OnPropertyChanged("Name"); } } public virtual void OnPropertyChanged(string propertyName) { PropertyChangedEventHandler propertyChanged = this.PropertyChanged; if (propertyChanged != null) { propertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
اصل هفتم: Liskove Substitution Principle
"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"
این اصل میگوید اگر قرار است
از ارث بری استفاده شود، نحوهی استفاده باید بدین گونه باشد که اگر یک شیء از کلاس
والد ( Base-Parent-Super type ) داشته باشیم، باید بتوان آن را
با شیء کلاس فرزند ( Sub
Type-Child ) بدون
هیچ گونه تغییری در منطق کد استفاده کننده از شیء مورد نظر، تغییر داد. به زبان
ساده باید بتوان شیء فرزند را جایگزین شیء والد کرد.
نکته مهم: این اصل در مورد عکس این رابطه صحبتی نمیکند و دلیل آن هم منطق طراحی میباشد. تصور کنید که شیء ای داشته باشید که از یک کلاس والد، ارث برده باشد. نوشتن کدی که شیء والد را بتوان جایگزین شیء فرزند کرد، بسیار سخت است؛ چرا که منطق متکی بر کلاس فرزند بسیار وابسته به جزییات کلاس فرزند است. در غیر این صورت وجود شیء فرزند، کم اهمیت میباشد.
با رعایت این اصل، میتوانیم در مواقعی که شروط مرتبط با کلاس فرزند را نداریم و یک سری منطق و قیود کلی مرتبط با کلاس والد را داریم، از شیء کلاس والد استفاده نماییم و وظیفه نمونه گیری (instantiation ) آن را به یک کلاس دیگر محول کنیم. به مثال زیر توجه کنید:
public class Parent { public string Name { get; set; } public int X { get; set; } public int Y { get; set; } public Parent() { X = Y = 0; } public virtual void Move() { X += 5; Y += 5; } public void Shoot() { } public virtual void Pass() { } } public class Child1 : Parent { public override void Move() { X += 10; Y += 10; } } public class Child2 : Parent { public override void Move() { X += 20; Y += 20; } } public enum State { Start, Move, Shoot, Pass } public class Creator { public static Parent GetInstance(bool? condition) { if (condition == null) { return new Parent(); } if (condition == true) { return new Child1(); } else { return new Child2(); } } } public class Context { public void SetState(ref State s) { s = State.Move; } public void Main() { State state =State.Start; // در مورد نوع این شیء چیزی نمیدانیم و وابسته به شرایط نوع آن متغیر است // در حقیقت شیء کلاس فرزند را جای شیء کلاس والد قرار میدهیم و نه بالعکس Parent obj = Creator.GetInstance(null); // منطق برنامه وضعیت را تغییر میدهد SetState(ref state); // قواعد کلی و عمومی که بدون در نظر گرفتن کلاس (نوع) شیء بر آن اعمال میشود switch (state) { case State.Move: obj.Move(); break; case State.Shoot: obj.Shoot(); break; case State.Pass: obj.Pass(); break; default: break; } } }
همانطور که در کدها نیز توضیح دادهام، کلاسهای فرزند را جایگزین کلاس والد کردهایم. اگر میخواستیم عکس رابطه را (شیء والد را به شیء فرزند انتقال دهیم) اعمال کنیم باید تغییر زیر را ایجاد میکردیم که با خطا روبرو خواهد شد:
Child1 obj = Creator.GetInstance(null);
اصل هشتم: Interface segregation
"واسطهای کوچک بهتر از واسطهای حجیم است"
این اصل به ما میگوید در تعریف واسطهای متعدد خساست به خرج ندهیم و بجای آنکه یک واسط اصلی با وظیفههای بسیار داشته باشیم، بهتر است واسطهای متعددی با وظیفههای کمتر داشته باشیم. برای درک این اصل ساده به عقب برمیگردیم، جایی که نیاز به واسط را توضیح دادیم. واسط، نقش تعریف پروتکل را دارد. اگر قرار باشد واسطی بزرگ با چندین مسئولیت داشته باشیم، آنگاه تعریف مستحکمی را از وظیفهی واسط ارائه ندادهایم. لذا هر کلاس پیاده ساز این واسط، برخی وظیفههایی را که نیاز به آن ندارد، باید تعریف و پیاده سازی کند. به مثال زیر نگاه کنید:
public interface IHuman { void Move(); void Eat(); void LevelUp(); void FireBullet(); } public class Player : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Enemy : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class Citizen : IHuman { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } }
در این مثال که مربوط به مدل یک بازی با نقشهای بازیکن، دشمن و شهروند (بی گناه!) است، طراحی به گونهای است که دشمن و شهروند، توابعی را که نیاز ندارند، باید پیاده سازی کنند. در دشمن: Eat(), LevelUp() و در شهروند: Eat(), LevelUp(), FireBullet() . لذا واسط IHuman یک واسط کلی با وظیفههای متعدد است.
در مدل بهبود یافته که کلاسها با پسوند Better بازنویسی شدهاند داریم:
public interface IMovable { void Move(); } public interface IEatable { void Eat(); } public interface IPlayer { void LevelUp(); } public interface IShooter { void FireBullet(); } public class PlayerBetter : IPlayer, IMovable, IEatable, IShooter { public void Eat() { } public void FireBullet() { } public void LevelUp() { } public void Move() { } } public class EnemyBetter : IMovable, IShooter { public void FireBullet() { } public void Move() { } } public class CitizenBetter : IMovable { public void Move() { } }
در اینجا برای هر وظیفه یک واسط تعریف کرده ایم که باعث قوی شدن معنای هر واسط میشود.
اصل نهم: Dependency inversion
"وابستگی بین ماژولها را به وابستگی آنها به انتزاع (واسط) تغییر بده"
این اصل که نمود آن را در الگوهای طراحی dependency injection و factory میبینیم، میگوید که ماژولهای بالادست (ماژول استفاده کننده ماژول پایین دست) به جای آنکه ارجاع مستقیمی را به ماژولهای پایین دست داشته باشند، به انتزاعی (واسط) ارجاع بدهند که ماژول پایین دست آنرا پیاده سازی میکند یا به ارث میبرد. در واقع این اصل برای از بین بردن وابستگی قوی بین ماژولهای بالا دست و پایین دست، به میدان آمده است. دو حکم اصلی از این اصل بر میآید:
الف – ماژولهای بالا دست نباید وابسته به ماژولهای پایین دست باشند. هر دو باید وابسته به انتزاع (واسط) باشند. وابستگی ماژول بالا دست از نوع ارجاع و وابستگی ماژول پایین دست از نوع ارث بری است.
ب – انتزاع نباید وابسته به جزییات باشد، بلکه جزییات باید وابسته به انتزاع باشد. یعنی در پیاده سازی منطق برنامه (که جزییات محسوب میشود) باید از واسطها یا کلاسهای انتزاعی استفاده کنیم و همچنین در نوشتن کلاسهای انتزاعی نباید هیچ گونه ارجاعی را به کلاسهای جزیی داشته باشیم.
در مثال زیر با نمونهای از طراحی ناقض این اصل روبرو هستیم:
public class Controller { public Service Service { get; set; } public Controller() { // کنترلر باید نحوه نمونه گیری را بداند (ورودیهای لازم) و این از وظایف آن خارج است Service = new Service(1); } public void DoWork() { Service.RunService(); } } public class Service { public int State { get; set; } public Service(int s) { State = s; } public void RunService() { } }
در این مثال کلاس کنترلر، ماژول بالادست و کلاس سرویس، ماژول پایین دست محسوب میگردد. در ادامه طراحی مطلوب را نیز ارائه دادهام:
public class ControllerBetter { // ارجاع به واسط باعث انعطاف و کاهش وابستگی شده است public IService Service { get; set; } public ControllerBetter(IService service) { // یک کلاس دیگر وظیفه ارسال سرویس به سازنده کلاس کنترلر را دارد // و مسئولیت نمونه گیری را از دوش کنترلر برداشته است Service = service; } public void DoWork() { Service.RunService(); } } // کاهش وابستگی با تعریف واسط و تغییر وابستگی مستقیم بین کنترلر و سرویس public interface IService { void RunService(); } // وابستگی جزییات به انتزاع public class ServiceBetter : IService { public int State { get; set; } public ServiceBetter(int s) { State = s; } public void RunService() { } }
نحوه بهبود طراحی را در توضیحات داخل کد مشاهده میکنید. در مقاله بعدی به اصول GRASP خواهم پرداخت.
namespace SameInterfaceDifferentClasses.Services.Contracts { public interface IMessageService { void Send(string message); } }
public class EmailService : IMessageService { public void Send(string message) { // ... } } public class SmsService : IMessageService { public void Send(string message) { //todo: ... } }
public interface IUsersManagerService { void ValidateUserByEmail(int id); } public class UsersManagerService : IUsersManagerService { private readonly IMessageService _emailService; private readonly IMessageService _smsService; public UsersManagerService(IMessageService emailService, IMessageService smsService) { _emailService = emailService; _smsService = smsService; } public void ValidateUserByEmail(int id) { _emailService.Send("Validated."); } }
ioc.For<IMessageService>().Use<SmsService>(); ioc.For<IMessageService>().Use<EmailService>();
برای حل این مشکل میتوان به نحو ذیل عمل کرد:
public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(ioc => { // map same interface to different concrete classes ioc.For<IMessageService>().Use<SmsService>(); ioc.For<IMessageService>().Use<EmailService>(); ioc.For<IUsersManagerService>().Use<UsersManagerService>() .Ctor<IMessageService>("smsService").Is<SmsService>() .Ctor<IMessageService>("emailService").Is<EmailService>(); }); } }
var usersManagerService = SmObjectFactory.Container.GetInstance<IUsersManagerService>(); usersManagerService.ValidateUserByEmail(id: 1);
همانطور که در تصویر مشخص است، هر کدام از پارامترها، توسط کلاسهای متفاوتی مقدار دهی شدهاند؛ هرچند از یک اینترفیس مشخص استفاده میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
Dependency-Injection-Samples/DI09