معرفی موجودیت Person
در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا میکنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System; namespace EFCoreDbFunctionsSample.Entities { public class Person { public int Id { get; set; } public string Name { get; set; } public DateTime AddDate { get; set; } } }
گزارشگیری بر اساس تعداد روز گذشتهی از ثبت نام
اکنون فرض کنید میخواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کردهاند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
روش تعریف تابع DATEDIFF سفارشی در EF Core
برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنهی آنرا صرفا با یک throw new InvalidOperationException مقدار دهی میکنیم. هدف از این متد، استفادهی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer { public enum SqlDateDiff { Year, Quarter, Month, DayOfYear, Day, Week, Hour, Minute, Second, MilliSecond, MicroSecond, NanoSecond } public static class SqlDbFunctionsExtensions { public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side."); public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions) .GetRuntimeMethod( nameof(SqlDbFunctionsExtensions.SqlDateDiff), new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) } ); } }
روش معرفی تابع DATEDIFF سفارشی به EF Core
پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer { public class ApplicationDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo) .HasTranslation(args => { var parameters = args.ToArray(); var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString(); return SqlFunctionExpression.Create("DATEDIFF", new[] { new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) . parameters[1], parameters[2] }, SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType, typeMapping: null); }); } } }
سپس توسط متد HasTranslation، مشخص میکنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار میگیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر میشوند. پارامترهای دوم و سوم آنرا به همان نحوی که دریافت میکنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کردهایم و همچنین قرار نیست به صورت 'N'day و رشتهای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آنرا استخراج کرده و سپس توسط SqlFragmentExpression عنوان میکنیم آنرا باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.
استفادهی از DbFunction سفارشی جدید در برنامه
پس از این تعاریف و معرفیها، اکنون میتوان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئریهای LINQ استفاده کرد تا قابلیت ترجمهی به SQL را پیدا کنند:
var sinceDays = 10; users = context.People.Where(person => SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList(); /* SELECT [p].[Id], [p].[AddDate], [p].[Name] FROM [People] AS [p] WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0 */
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
- لیستی از حقوق کارکنان را داریم. در گزارش نهایی آن نیاز است عدد حقوق کارکنانی با مبلغ کمتر از 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 بازگشت دهید. برای نمونه در مثال فوق، یک جدول را در سلول تعریف شده قرار دادهایم. این جدول یک ستون دارد و هر سلولی که به آن اضافه خواهد شد، یک ردیف را تشکیل خواهد داد. در ردیف اول آن تصویر قرار گرفته و در ردیف دوم آن مقدار سلول جاری.
خالق جیکوئری (John Resig)، این کتابخانه را در سالهای 2006 زمانیکه Internet Explorer نگارشهای 6 و 7 بیش از 60 درصد بازار مرورگرها را به خود اختصاص داده بودند، ارائه داد. بله؛ در آْن زمان JavaScript Web API بسیار خام، پایداری مرورگرها بسیار پایین و تطابق با استانداردهای وب در بین مرورگرهای مختلف نیز بسیار پایین بود. بنابراین علت محبوبیت کتابخانهای که در این شرایط، تجربهی کاری یکدستی را در بین مرورگرهای مختلف ارائه میداد، کاملا واضح بود. اما ... اکنون سال 2018 است و حتی مایکروسافت هم دیگر از نگارشهای مختلف IE پشتیبانی نمیکند. DOM API موجود در مرورگرهای مدرن بسیار توانمند شدهاند و در بین انواع و اقسام آنها یکدست عمل میکنند. حتی اگر دلیل استفادهی از jQuery ایجاد سادهتر حلقهها بر روی اشیاء جاوا اسکریپتی باشد (رفع کمبودهای جاوا اسکریپت)، از زمان IE 9 به بعد، متدهای forEach و Object.keys به صورت توکار در جاوا اسکریپت وجود دارند و یا اگر نیاز به inArray.$ داشته باشید، متد Array.prototype.indexOf مدتها است که جزئی از ES5 است. به همین جهت است که این روزها اخباری را مانند «GitHub نیز جیکوئری را کنار گذاشت» زیاد میشنوید. نه فقط کنار گذاشتن jQuery یک وابستگی ثالث را از برنامه حذف میکند، بلکه کار مستقیم با native API مرورگرها همواره به مراتب سریعتر است از کتابخانههایی که سطح بالایی از abstraction آنها را ارائه میدهند.
یافتن عناصر توسط JavaScript خالص
زمانیکه نیاز به انتخاب عناصری در صفحه باشند، بلافاصله ذهن ما به سمت ('myElement#')$ و ('myElement.')$ جیکوئری، معطوف میشود. اما ... این روزها برای انجام این نوع کارها واقعا نیازی به jQuery نیست!
یافتن عناصر بر اساس ID آنها
<div id="my-element-id"></div>
var result = $('#my-element-id');
انجام این کار توسط web API و یا همان JavaScript خالص، چنین شکلی را دارد:
var result = document.getElementById('my-element-id');
روش دیگر انجام اینکار با JavaScript به صورت زیر است:
var result = document.querySelector('#my-element-id');
در هر دو حالت، خروجی مقایسهی ذیل، true است:
result.id === 'my-element-id'; // returns true
یافتن عناصر بر اساس کلاسهای CSS
<span class="some-class"></span>
var result = $('.some-class');
var result = document.getElementsByClassName('some-class');
var result = document.querySelectorAll('.some-class');
result[0].className === 'some-class'; // returns true
یافتن عناصر بر اساس تگهای عناصر
<code>Console.WriteLine("Hello world!");</code>
var result = $('code');
var result = document.getElementsByTagName('code');
var result = document.querySelectorAll('code');
result[0].tagName === 'code'; // returns true
یافتن عناصر بر اساس کلاس نماها (Pseudo-classes)
Pseudo-classes از زمان ابتداییترین پیشنویس استاندارد CSS وجود داشتهاند. برای مثال visited: یک Pseudo-classes است و به لینکهای بازدید شدهی توسط کاربر اشاره میکند و یا focus: به المانی اشاره میکند که هم اکنون دارای focus است.
<form> <label>Full Name <input name="full-name"> </label> <label>Company <input name="company"> </label> </form>
var focusedInputs = $('INPUT:focus');
var companyInput = document.querySelector('INPUT:focus');
یافتن عناصر بر اساس ارتباط والد و فرزندی آنها
<div> <a href="https://www.dntips.ir"> <span>Go to site</span> </a> <p>Some text</p> Some other text </div>
روش یافتن والد anchor tag در جیکوئری توسط متد parent؛ با فرض اینکه a$ به شیء anchor اشاره میکند:
var $result = $a.parent();
var result = a.parentNode;
یافتن فرزندان:
در جیکوئری:
var result = $('#myParent').children();
var result = document.querySelectorAll('DIV > *');
var result = document.querySelectorAll('DIV > P');
var result = document.getElementById('myParent').childNodes; var result = div.childNodes;
var result =document.getElementById('myParent').children;
var result =document.querySelectorAll('#myParent A');
جستجوی عناصر با صرفنظر کردن از بعضی از آنها
<ul role="menu"> <li>choice 1</li> <li class="active">choice 2</li> <li>choice 3</li> </ul>
var $result = $('UL LI').not('.active');
var result = document.querySelectorAll('UL LI:not(.active)');
انتخاب چندین المان با هم
<div id="link-container"> <a href="https://github.com/VahidN">GitHub</a> </div> <ol> <li>one</li> <li>two</li> </ol> <span class="my-name">VahidN</span>
var $result = $('#link-container, .my-name, OL');
var result = document.querySelectorAll('#link-container, .my-name, OL');
یافتن گروهی از المانها بر اساس نوع آنها:
var result = document.querySelectorAll( 'BUTTON[type="submit"], INPUT[type="submit"]' );
جایگزین کردن $ جیکوئری با جاوا اسکریپت
تا اینجا حتما به شباهت کدهای خالص جاوا اسکریپت و jQuery دقت کردهاید. اگر بخواهیم برای $ جیکوئری، یک معادل جاوا اسکریپتی تهیه کنیم، به قطعه کد زیر خواهیم رسید:
window.$ = function(selector) { return document.querySelectorAll(selector); };
$('.some-class'); $('#some-id'); $('.some-parent > .some-child'); $('UL LI:not(.active)');
public class Product { public Product(string name) { Name = name; } public string Name { get; set; } public string Screen { get; set; } public string OS { get; set; } public override string ToString() { return string.Format(Screen + "/" + OS + "/" + Name); } }
public interface IBuilder { void BuildScreen(); void BuildOS(); Product Product { get; } }
public class ConcreteBuilder1 : IBuilder { public Product p; public ConcreteBuilder1() { p = new Product("Android Cell Phone"); } public void BuildScreen() { p.Screen = "Touch Screen 16 Inch!"; } public void BuildOS() { p.OS = "Android 4.4"; } public Product Product { get { return p; } } }
public class ConcreteBuilder2 : IBuilder { public Product p; public ConcreteBuilder2() { p = new Product("Windows Phone"); } public void BuildScreen() { p.Screen = "Touch Screen 32 Inch!"; } public void BuildOS() { p.OS = "Windows Phone 2014"; } public Product Product { get { return p; } } }
public class Director { public void Construct(IBuilder builder) { builder.BuildScreen(); builder.BuildOS(); } }
Director d = new Director(); ConcreteBuilder1 cb1 = new ConcreteBuilder1(); d.Construct(cb1); Console.WriteLine(cb1.p.ToString());
public class BlackBerry: IBuilder { public Product p; public BlackBerry () { p = new Product("BlackBerry"); } public void BuildScreen() { p.Screen = "Touch Screen 8 Inch!"; } public void BuildOS() { p.OS = "BlackBerry XXX"; } public Product Product { get { return p; } } }
private static void Query10() { using (var context = new StoreDbContext()) { var customers = context.Customers; foreach (var customer in customers) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); foreach (var order in customer.Orders) { Console.WriteLine("\t Order Date: {0}", order.Date); } } } }
- Lazy Loading
- Eager Loading
- Explicit Loading
private static void Query11() { using (var context = new StoreDbContext()) { var customer = context.Customers.First(); Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); foreach (var order in customer.Orders) { Console.WriteLine("\t Order Date: {0}", order.Date); } } }
context.Configuration.LazyLoadingEnabled = false;
private static void Query12() { using (var context = new StoreDbContext()) { var customers = context.Customers; foreach (var customer in customers) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); foreach (var order in customer.Orders) { Console.WriteLine("\t Order Date: {0}", order.Date); } } } }
private static void Query13() { using (var context = new StoreDbContext()) { var customers = context.Customers.Include(c => c.Orders); //var customers = context.Customers.Include("Orders"); foreach (var customer in customers) { Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family); foreach (var order in customer.Orders) { Console.WriteLine("\t Order Date: {0}", order.Date); } } }
context.OrderDetails.Include(o => o.Order.Customer)
context.Orders.Include(o => o.OrderDetail.Select(od => od.Product))
context.Orders.Include(o => o.Customer).Include(o => o.OrderDetail)
روش Explicit Loading: این روش مانند Lazy Loading میباشد که میتوان دادههای مرتبط را جداگانه فراخوانی کرد اما نه به صورت اتوماتیک توسط EF بلکه به صورت صریح توسط خودمان انجام میشود. این روش حتی اگر navigation propertyهای ما virtual نباشند نیز قابل انجام است. برای انجام این روش از متد DbContext.Entry استفاده میشود.
private static void Query14() { using (var context = new StoreDbContext()) { var customer = context.Customers.First(c => c.Family == "Jamshidi"); context.Entry(customer).Collection(c => c.Orders).Load(); foreach (var order in customer.Orders) { Console.WriteLine(order.Date); } } }
private static void Query15() { using (var context = new StoreDbContext()) { var order = context.Orders.First(); context.Entry(order).Reference(o => o.Customer).Load(); Console.WriteLine(order.Customer.FullName); } }
if (context.Entry(order).Reference(o => o.Customer).IsLoaded) context.Entry(order).Reference(o => o.Customer).Load();
private static void Query16() { using (var context = new StoreDbContext()) { var customer = context.Customers.First(c => c.Family == "Jamshidi"); IQueryable<Order> query = context.Entry(customer).Collection(c => c.Orders).Query(); var order = query.First(); } }
آماده سازی برنامههای دات نت برای دسترسی به API مترو ویندوز 8.1
ابتدا یک برنامهی کنسول دات نت 4.5.1 را آغاز کنید. برای دسترسی به API ویندوز 8.1 حتما نیاز است که حداقل از دات نت 4.5.1 شروع کرد. سپس برنامه را در VS.NET بسته و فایل پروژه آنرا در یک ادیتور متنی باز کنید.
در ابتدای فایل csproj، نیاز است سطر TargetPlatformVersion ذیل اضافه شود.
<PropertyGroup> <TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion> <TargetPlatformVersion>8.1</TargetPlatformVersion> </PropertyGroup>
<ItemGroup> <Reference Include="System" /> <Reference Include="System.ComponentModel.DataAnnotations" /> <Reference Include="System.Core" /> <Reference Include="System.ObjectModel" /> <Reference Include="System.Xml.Linq" /> <Reference Include="System.Data.DataSetExtensions" /> <Reference Include="Microsoft.CSharp" /> <Reference Include="System.Data" /> <Reference Include="System.Xml" /> <Reference Include="System.Threading" /> <Reference Include="System.Threading.Tasks" /> </ItemGroup> <ItemGroup> <Reference Include="Windows" /> <Reference Include="System.Runtime" /> <Reference Include="System.Runtime.WindowsRuntime" /> </ItemGroup>
یک نکته
اگر میخواهید این فرآیند را ساده و خودکار کنید، از قالبهای پروژهی مخصوص DesktopWinRT.Templates.vsix استفاده نمائید.
DesktopWinRT.Templates.vsix
افزودن ارجاعی به Nito.AsyncEx
چون برنامهی مورد استفاده کنسول است و API ویندوز 8 کاملا async طراحی شدهاست، نیاز است با کمک AsyncContext موجود در کتابخانهی Nito.AsyncEx بتوان از امکانات async و await در متد Main برنامه استفاده کرد. البته اگر از سایر برنامههای دسکتاپ استفاده میکنید، فقط کافی است امضای متد رخدادن گردان را به async تغییر دهید.
install-package Nito.AsyncEx
تبدیل استریمهای دات نت به استریمهای WinRT
اکثر متدهای WinRT با استریمهایی از نوع IRandomAccessStream کار میکنند. برای اینکه بتوان استریم استاندارد دات نت را به این نوع تبدیل کرد، میتوان از کلاسهای ذیل کمک گرفت:
using System; using System.IO; using Windows.Storage.Streams; namespace ConsoleWin81PdfApiTest { public static class MicrosoftStreamExtensions { public static IRandomAccessStream AsRandomAccessStream(this Stream stream) { return new RandomStream(stream); } } class RandomStream : IRandomAccessStream { readonly Stream _internstream; public RandomStream(Stream underlyingstream) { _internstream = underlyingstream; } public IInputStream GetInputStreamAt(ulong position) { _internstream.Position = (long)position; return _internstream.AsInputStream(); } public IOutputStream GetOutputStreamAt(ulong position) { _internstream.Position = (long)position; return _internstream.AsOutputStream(); } public ulong Size { get { return (ulong)_internstream.Length; } set { _internstream.SetLength((long)value); } } public bool CanRead { get { return _internstream.CanRead; } } public bool CanWrite { get { return _internstream.CanWrite; } } public IRandomAccessStream CloneStream() { throw new NotSupportedException(); } public ulong Position { get { return (ulong)_internstream.Position; } } public void Seek(ulong position) { _internstream.Seek((long)position, SeekOrigin.Begin); } public void Dispose() { _internstream.Dispose(); } public Windows.Foundation.IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options) { return GetInputStreamAt(Position).ReadAsync(buffer, count, options); } public Windows.Foundation.IAsyncOperation<bool> FlushAsync() { return GetOutputStreamAt(Position).FlushAsync(); } public Windows.Foundation.IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer) { return GetOutputStreamAt(Position).WriteAsync(buffer); } } }
خواندن فایلهای PDF و تبدیل صفحات آنها به تصویر
در ادامه کد کامل استفاده از API جدید ویندوز 8.1 را جهت خواندن فایلهای PDF ملاحظه میکنید. این امکانات جدید در فضای نام Windows.Data.Pdf قرار دارند و صرفا امکان خواندن فایلهای PDF را تدارک دیدهاند.
using System; using System.IO; using System.Threading.Tasks; using Windows.Data.Pdf; using Nito.AsyncEx; namespace ConsoleWin81PdfApiTest { class Program { static void Main(string[] args) { AsyncContext.Run(async () => { await test(); }); } private static async Task test() { using (var randomAccessStream = File.Open("PieChartPdfReport.pdf", FileMode.Open).AsRandomAccessStream()) { var pdfDocument = await PdfDocument.LoadFromStreamAsync(randomAccessStream); for (uint i = 0; i < pdfDocument.PageCount; i++) { using (var page = pdfDocument.GetPage(i)) { /*var renderOptions = new PdfPageRenderOptions { BackgroundColor = Colors.LightGray, DestinationHeight = (uint) (page.Size.Height*10) };*/ using (var stream = File.Open(string.Format("page-{0}.png", i + 1), FileMode.OpenOrCreate).AsRandomAccessStream()) { await page.RenderToStreamAsync(stream/*, renderOptions*/); await stream.FlushAsync(); } } } } } } }
توضیحات:
- متد AsyncContext.Run جزو امکانات Nito.AsyncEx است و امکان نوشتن کدهای await دار را در متد Main یک برنامهی کنسول فراهم میکند.
- متد File.Openدات نت، خروجی از نوع استریم دارد. برای تبدیل آن به نوع IRandomAccessStream، از متد الحاقی AsRandomAccessStream که پیشتر تهیه کردیم، میتوان استفاده کرد.
- در ادامه متد PdfDocument.LoadFromStreamAsync این استریم خاص را دریافت کرده و امکان دسترسی به API ویندوز 8.1 را میسر میکند.
- توسط متد pdfDocument.GetPage میتوان به صفحات مختلف فایل PDF باز شده دسترسی یافت. در اینجا متد page.RenderToStreamAsync، سبب رندر شدن صفحه با فرمت PNG میشود. این خروجی نهایتا باید در یک استریم از نوع IRandomAccessStream ثبت شود. در اینجا نیز میتوان از متد File.Open در حالت FileMode.OpenOrCreate استفاده کرد.
- اگر میخواهید ابعاد تصویر نهایی و ویژگیهای آنرا تغییر دهید، میتوان از پارامتر دوم متد page.RenderToStreamAsync استفاده کرد که شیءایی از نوع PdfPageRenderOptions را میپذیرد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید
MicrosoftStreamExtensions.zip
برای مطالعه بیشتر
How to use specific WinRT API from Desktop apps
How to call WinRT APIs from .NET desktop apps
ایجاد «خواص الحاقی»
احتمالا شاید عنوان کنید که با اشیاء dynamic میتوان چنین کاری را انجام داد. اما سؤال در مورد اشیاء غیر dynamic است.
یا نمونهی دیگر آن Attached Properties در برنامههای مبتنی بر Xaml هستند. میتوان به یک شیء از پیش موجود Xaml، خاصیتی را افزود که البته پیاده سازی آن منحصر است به همان نوع برنامهها.
راه حل پیشنهادی
یک Dictionary را ایجاد کنیم تا ارجاعی از اشیاء، به عنوان کلید، در آن ذخیره شده و سپس key/valueهایی به عنوان value هر شیء، در آن ذخیره شوند. این key/valueها همان خواص و مقادیر آنها خواهند بود. هر چند این راه حل به خوبی کار میکند اما ... مشکل نشتی حافظه دارد.
شیء Dictionary یک ارجاع قوی را از اشیاء، درون خودش نگه داری میکند و تا زمانیکه در حافظه باقی است، سیستم GC مجوز رهاسازی منابع آنها را نخواهد یافت؛ چون عموما این نوع Dictionaryها باید استاتیک تعریف شوند تا طول عمر آنها با طول عمر برنامه یکی گردد. بنابراین اساسا اشیایی که به این نحو قرار است پردازش شوند، هیچگاه dispose نخواهند شد. راه حلی برای این مساله در دات نت 4 به صورت توکار به دات نت فریم ورک اضافه شدهاست؛ به نام ساختار دادهای ConditionalWeakTable.
معرفی ConditionalWeakTable
ConditionalWeakTable جزو ساختارهای دادهای کمتر شناخته شدهی دات نت است. این ساختار داده، اشارهگرهایی را به ارجاعات اشیاء، درون خود ذخیره میکند. بنابراین چون ارجاعاتی قوی را به اشیاء ایجاد نمیکند، مانع عملکرد GC نیز نشده و برنامه در دراز مدت دچار مشکل نشتی حافظه نخواهد شد. هدف اصلی آن ایجاد ارتباطی بین CLR و DLR است. توسط آن میتوان به اشیاء دلخواه، خواصی را افزود. به علاوه طراحی آن به نحوی است که thread safe است و مباحث قفل گذاری بر روی اطلاعات، به صورت توکار در آن پیاده سازی شدهاست. کار DLR فراهم آوردن امکان پیاده سازی زبانهای پویایی مانند Ruby و Python برفراز CLR است. در این نوع زبانها میتوان به وهلههایی از اشیاء موجود، خاصیتهای جدیدی را متصل کرد.
به صورت خلاصه کار ConditionalWeakTable ایجاد نگاشتی است بین وهلههایی از اشیاء CLR (اشیایی غیرپویا) و خواصی که به آنها میتوان به صورت پویا انتساب داد. در کار GC اخلال ایجاد نمیکند و همچنین میتوان به صورت همزمان از طریق تردهای مختلف، بدون مشکل با آن کار کرد.
پیاده سازی خواص الحاقی به کمک ConditionalWeakTable
در اینجا نحوهی استفاده از ConditionalWeakTable را جهت اتصال خواصی جدید به وهلههای موجود اشیاء مشاهده میکنید:
using System.Collections.Generic; using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class AttachedProperties { public static ConditionalWeakTable<object, Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object, Dictionary<string, object>>(); public static void SetValue<T>(this T obj, string name, object value) where T : class { var properties = ObjectCache.GetOrCreateValue(obj); if (properties.ContainsKey(name)) properties[name] = value; else properties.Add(name, value); } public static T GetValue<T>(this object obj, string name) { Dictionary<string, object> properties; if (ObjectCache.TryGetValue(obj, out properties) && properties.ContainsKey(name)) return (T)properties[name]; return default(T); } public static object GetValue(this object obj, string name) { return obj.GetValue<object>(name); } } }
کاربرد اول
اگر با ASP.NET کار کرده باشید حتما با IPrincipal آشنایی دارید. خواصی مانند Identity یک کاربر در آن ذخیره میشوند.
سؤال: چگونه میتوان یک خاصیت جدید به نام مثلا Disclaimer را به وهلهای از این شیء افزود:
public static class ISecurityPrincipalExtension { public static bool Disclaimer(this IPrincipal principal) { return principal.GetValue<bool>("Disclaimer"); } public static void SetDisclaimer(this IPrincipal principal, bool value) { principal.SetValue("Disclaimer", value); } }
اگر صرفا قرار است یک خاصیت به شیءایی متصل شود، روش ذیل نیز قابل استفاده میباشد (بجای استفاده از دیکشنری از یک کلاس جهت تعریف خاصیت اضافی جدید استفاده شدهاست):
using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class PropertyExtensions { private class ExtraPropertyHolder { public bool IsDirty { get; set; } } private static readonly ConditionalWeakTable<object, ExtraPropertyHolder> _isDirtyTable = new ConditionalWeakTable<object, ExtraPropertyHolder>(); public static bool IsDirty(this object @this) { return _isDirtyTable.GetOrCreateValue(@this).IsDirty; } public static void SetIsDirty(this object @this, bool isDirty) { _isDirtyTable.GetOrCreateValue(@this).IsDirty = isDirty; } } }
کاربرد دوم
ایجاد Id منحصربفرد برای اشیاء برنامه.
فرض کنید در حال نوشتن یک Entity framework profiler هستید. طراحی فعلی سیستم Interception آن به نحو زیر است:
public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { }
راه حل: خوب ... یک خاصیت Id را به اشیاء موجود متصل کنید!
using System; using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class UniqueIdExtensions { static readonly ConditionalWeakTable<object, string> _idTable = new ConditionalWeakTable<object, string>(); public static string GetUniqueId(this object obj) { return _idTable.GetValue(obj, o => Guid.NewGuid().ToString()); } public static string GetUniqueId(this object obj, string key) { return _idTable.GetValue(obj, o => key); } } }
برای مطالعه بیشتر
The Conditional Weak Table: Enabling Dynamic Object Properties
How to create mixin using C# 4.0
Disclaimer Page using MVC
Extension Properties Revised
Easy Modeling
Providing unique ID on managed object using ConditionalWeakTable
Blazor به صورت توکار به همراه تعدادی کنترل ورودی مانند InputText، InputTextArea، InputSelect، InputNumber، InputCheckbox و InputDate است که با سیستم اعتبارسنجی ورودیهای آن نیز یکپارچه هستند.
در یک برنامهی واقعی نیاز است divهایی مانند زیر را که به همراه روش تعریف این کامپوننتهای ورودی است، صدها بار در قسمتهای مختلف تکرار کرد:
<EditForm Model="NewPerson" OnValidSubmit="HandleValidSubmit"> <DataAnnotationsValidator /> <div class="form-group"> <label for="firstname">First Name</label> <InputText @bind-Value="NewPerson.FirstName" class="form-control" id="firstname" /> <ValidationMessage For="NewPerson.FirstName" /> </div>
تمام کامپوننتهای ورودی Blazor از کلاس پایهی ویژهای به نام <InputBase<T مشتق شدهاند. این کلاس است که کار یکپارچگی با EditContext را جهت ارائهی اعتبارسنجیهای لازم، انجام میدهد. همچنین کار binding را نیز با ارائهی پارامتر Value از نوع T انجام میدهد که نوشتن یک چنین کدهایی مانند "bind-Value="myForm.MyValue@ را میسر میکند. InputBase یک کلاس جنریک است که خاصیت Value آن از نوع T است. از آنجائیکه مرورگرها اطلاعات را به صورت رشتهای در اختیار ما قرار میدهند، این کامپوننت نیاز به روشی را دارد تا بتواند ورودی دریافتی را به نوع T تبدیل کند و اینکار را میتوان با بازنویسی متد TryParseValueFromString آن انجام داد:
protected abstract bool TryParseValueFromString(string value, out T result, out string validationErrorMessage);
یک مثال: کامپوننت جدید Shared\InputPassword.razor
@inherits InputBase<string> <input type="password" @bind="@CurrentValue" class="@CssClass" /> @code { protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage) { result = value; validationErrorMessage = null; return true; } }
<EditForm Model="userInfo" OnValidSubmit="CreateUser"> <DataAnnotationsValidator /> <InputPassword class="form-control" @bind-Value="@userInfo.Password" />
- در این مثال CurrentValue و CssClass از کلاس پایهی InputBase تامین میشوند.
- هربار که مقدار ورودی وارد شدهی توسط کاربر تغییر کند، متد TryParseValueFromString اجرا میشود.
- در متد TryParseValueFromString، مقدار validationErrorMessage به نال تنظیم شده؛ یعنی اعتبارسنجی خاصی مدنظر نیست. اولین پارامتر آن مقداری است که از کاربر دریافت شده و دومین پارامتر آن مقداری است که به کامپوننت ورودی که از آن ارثبری کردهایم، ارسال میشود تا CurrentValue را تشکیل دهد (و یا خاصیت CurrentValueAsString نیز برای این منظور وجود دارد).
- اگر اعتبارسنجی اطلاعات ورودی در متد TryParseValueFromString با شکست مواجه شود، مقدار false را باید بازگشت داد.
StringBuilder
بهترین روش برای تولید و دستکاری یک رشته (string) طولانی و یا دستکاری متناوب و تکراری یک رشته استفاده از کلاس StringBuilder است. این کلاس در فضای نام System.Text قرار داره. شی String در داتنتفریمورک تغییرناپذیره (immutable)، بدین معنی که پس از ایجاد نمیتوان محتوای اونو تغییر داد. برای مثال اگر شما بخواین محتوای یک رشته رو با اتصال به رشتهای دیگه تغییر بدین، اجازه اینکار را به شما داده نمیشه. درعوض بهصورت خودکار رشتهای جدید در حافظه ایجاد میشه و محتوای دو رشته موجود پس از اتصال به هم درون اون قرار میگیره. این کار درصورتیکه تعداد عملیات مشابه زیاد باشه میتونه تاثیر منفی بر کارایی و حافظه خالی در دسترس برنامه بگذاره.
کلاس StringBuilder با استفاده از آرایهای از کاراکترها، راهحل مناسب و بهینهای رو برای این مشکل فراهم کرده. این کلاس در زمان اجرا به شما اجازه میده تا بدون ایجاد نمونههای جدید از کلاس String، محتوای یک رشته رو تغییر بدین. شما میتونید نمونهای از این کلاس رو بهصورت خالی و یا با یک رشته اولیه ایجاد کنید، سپس با استفاده از متدهای متنوع موجود، محتوای رشته رو با استفاده از انواع داده مختلف و بهصورت دلخواه دستکاری کنید. همچنین با استفاده از متد معروف ()ToString این کلاس میتونید در هر لحظه دلخواه رشته تولیدی رو بدست بیارین. دو پراپرتی مهم کلاس StringBuilder رفتارش رو درهنگام افزودن دادههای جدید کنترل میکنن:
Capacity , Length
پراپرتی Capacity اندازه بافر کلاس StringBuilder را تعیین میکنه و Length طول رشته جاری موجود در این بافر رو نمایش میده. اگر پس از افزودن داده جدید، طول رشته از اندازه بافر موجود بیشتر بشه، StringBuilder باید یه بافر جدید با اندازهای مناسب ایجاد کنه تا رشته جدید رو بتونه تو خودش نگه داره. اندازه این بافر جدید بهصورت پیشفرض دو برابر اندازه بافر قبلی درنظر گرفته میشه. بعد تمام رشته قبلی رو تو این بافر جدید کپی میکنه.
از برنامه ساده زیر میتونین برای بررسی این مسئله استفاده کنین:
using System.IO; using System.Text; class Program { static void Main() { using (var writer = new StreamWriter("data.txt")) { var builder = new StringBuilder(); for (var i = 0; i <= 256; i++) { writer.Write(builder.Capacity); writer.Write(","); writer.Write(builder.Length); writer.WriteLine(); builder.Append('1'); // <-- Add one character } } } }
دقت کنین که برای افزودن یک کاراکتر استفاده از دستور Append با نوع داده char (همونطور که در بالا استفاده شده) بازدهی بهتری نسبت به استفاده از نوع داده string (با یک کاراکتر) داره. خروجی کد فوق به صورت زیره:
16, 0 16, 1 16, 2 ... 16,14 16,15 16,16 32,17 ...
استفاده نامناسب و بیدقت از این کلاس میتونه منجر به بازسازیهای متناوب این بافر شده که درنهایت فواید کلاس StringBuilder رو تحت تاثیر قرار میده. درهنگام کار با کلاس StringBuilder اگر از طول رشته موردنظر و یا حد بالایی برای Capacity آن آگاهی حتی نسبی دارین، میتونید با مقداردهی مناسب این پراپرتی از این مشکل پرهیز کنید.
نکته: مقدار پیشفرض پراپرتی Capacity برابر 16 است.
هنگام مقداردهی پراپرتیهای Capacity یا Length به موارد زیر توجه داشته باشید:
- مقداردهی Capacity به میزانی کمتر از طول رشته جاری (پراپرتی Length)، منجر به خطای زیر میشه:
System.ArgumentOutOfRangeException
خطای مشابهی هنگام مقداردهی پراپرتی Capacityبه بیش از مقدار پراپرتی MaxCapacity رخ میدهه.البته این مورد تنها درصورتیکه بخواین اونو به بیش از حدود 2 گیگابایت (Int32.MaxValue) مقداردهی کنید پیش میاد!
- اگر پراپرتی Length را به مقداری کمتر از طول رشته جاری تنظیم کنید، رشته به اندازه طول تنظیمی کوتاه (truncate) میشه.
- اگر مقدار پراپرتی Length را به میزانی بیشتر از طول رشته جاری تنظیم کنید، فضای خالی موجود در بافر با space پر میشه.
- تنظیم مقدار Length بیشتر از Capacity، منجر به مقداردهی خودکار پراپرتی Capacity به مقدار جدید تنظیم شده برای Length میشه.
در ادامه به یک مثال برای مقایسه کارایی تولید یک رشته طولانی با استفاده از این کلاس میپردازیم. تو این مثال از دو روش برای تولید رشتههای طولانی استفاده میشه. روش اول که همون روش اتصال رشتهها (Concat) به هم هستش و روش دوم هم که استفاده از کلاس StringBuilder است. در قطعه کد زیر کلاس مربوط به عملیات تست رو مشاهده میکنین:
namespace StringBuilderTest { internal class SbTest1 { internal Action<string> WriteLog; internal int Iterations { get; set; } internal string TestString { get; set; } internal SbTest1(int iterations, string testString, Action<string> writeLog) { Iterations = iterations; TestString = testString; WriteLog = writeLog; } internal void StartTest() { var watch = new Stopwatch(); //StringBuilder watch.Start(); var sbTestResult = SbTest(); watch.Stop(); WriteLog(string.Format("StringBuilder time: {0}", watch.ElapsedMilliseconds)); //Concat watch.Start(); var concatTestResult = ConcatTest(); watch.Stop(); WriteLog(string.Format("ConcatTest time: {0}", watch.ElapsedMilliseconds)); WriteLog(string.Format("Results are{0} the same", sbTestResult == concatTestResult ? string.Empty : " NOT")); } private string SbTest() { var sb = new StringBuilder(TestString); for (var x = 0; x < Iterations; x++) { sb.Append(TestString); } return sb.ToString(); } private string ConcatTest() { string concat = TestString; for (var x = 0; x < Iterations; x++) { concat += TestString; } return concat; } } }
دو روش بحثشده در کلاس مورد استفاده قرار گرفته و مدت زمان اجرای هر کدوم از عملیاتها به خروجی فرستاده میشه. برای استفاده از این کلاس هم میشه از کد زیر در یک برنامه کنسول استفاده کرد:
do { Console.Write("Iteration: "); var iterations = Convert.ToInt32(Console.ReadLine()); Console.Write("Test String: "); var testString = Console.ReadLine(); var test1 = new SbTest1(iterations, testString, Console.WriteLine); test1.StartTest(); Console.WriteLine("----------------------------------------------------------------"); } while (Console.ReadKey(true).Key == ConsoleKey.C); // C = continue
برای نمونه خروجی زیر در لپتاپ من (Corei7 2630QM) بدست اومد:
تنظیم خاصیت Capacity به یک مقدار مناسب میتونه تو کارایی تاثیرات زیادی بگذاره. مثلا در مورد مثال فوق میشه یه متد دیگه برای آزمایش تاثیر این مقداردهی به صورت زیر به کلاس برناممون اضافه کنیم:
private string SbCapacityTest() { var sb = new StringBuilder(TestString) { Capacity = TestString.Length * Iterations }; for (var x = 0; x < Iterations; x++) { sb.Append(TestString); } return sb.ToString(); }
تو این متد قبل از ورود به حلقه مقدار خاصیت Capacity به میزان موردنظر تنظیم شده و نتیجه بدست اومده:
مشاهده میشه که روش concat خیلی کنده (دقت کنین که طول رشته اولیه هم بیشتر شده) و برای ادامه کار مقایسه اون رو کامنت کردم و نتایج زیر بدست اومد:
میبینین که استفاده مناسب از مقداردهی به خاصیت Capacity میتونه تا حدود 300 درصد سرعت برنامه ما رو افزایش بده. البته همیشه اینطوری نخواهد بود. ما در این مثال مقدار دقیق طول رشته نهایی رو میدونستیم که باعث میشه عملیات افزایش بافر کلاس StringBuilder هیچوقت اتفاق نیفته. این امر در واقعیت کمتر پیش میاد.
مقاله موجود در سایت dotnetperls شکل زیر رو به عنوان نتیجه تست بازدهی ارائه میده:
- در مواقعی که عملیاتی همچون مثال بالا طولانی و حجیم ندارین بهتره که از این کلاس استفاده نکنین چون عملیاتهای داخلی این کلاس در عملیات کوچک و سبک (مثل ابتدای نمودار فوق) موجب کندی عملیات میشه. همچنین استفاده از اون نیاز به کدنویسی بیشتری داره.
- این کلاس فشار کمتری به حافظه سیستم وارد میکنه. درمقابل استفاده از روش concat موجب اشغال بیش از حد حافظه میشه که خودش باعث اجرای بیشتر و متناوبتر GC میشه که در نهایت کارایی سیستم رو کاهش میده.
- استفاده از این کلاس برای عملیات Replace (و یا عملیات مشابه) در حلقهها جهت کار با رشتههای طولانی و یا تعداد زیادی رشته میتونه بسیار سریعتر و بهتر عمل کنه چون این کلاس برخلاف کلاس string اشیای جدید تولید نمیکنه.
- یه اشتباه بزرگ در استفاده از این کلاس استفاده از "+" برای اتصال رشتههای درون StringBuilder هست. هرگز از این کارها نکنین. (فکر کنم واضحه که چرا)