MVVM و رویدادگردانی
در دو قسمت قبل به اینجا رسیدیم که بجای شروع به کدنویسی مستقیم در code behind یک View (یک پنجره، یک user control ...)، کلاس مجزای دیگری را به نام ViewModel به برنامه اضافه خواهیم کرد و این کلاس از وجود هیچ فرمی در برنامه مطلع نیست.
بنابراین جهت انتقال رخدادها به ViewModel، بجای روش متداول تعریف روالهای رخدادگردان در Code behind:
<Button Click="btnClick_Event">Last</Button>
آنها را با Commands به ViewModel ارسال خواهیم کرد:
<Button Command="{Binding GoLast}">Last</Button>
همچنین بجای اینکه مستقیما بخواهیم از طریق نام یک شیء به مثلا خاصیت متنی آن دسترسی پیدا کنیم:
<TextBox Name="txtName" />
از طریق Binding، اطلاعات مثلا متنی آنرا به ViewModel منتقل خواهیم کرد:
<TextBox Text="{Binding Name}" />
و همینجا است که 99 درصد آموزشهای MVVM موجود در وب به پایان میرسند؛ البته پس از مشاهده 10 تا 20 ویدیو و خواندن بیشتر از 30 تا مقاله! و اینجا است که خواهید گفت: فقط همین؟! با اینها میشه یک برنامه رو مدیریت کرد؟!
البته همینها برای مدیریت قسمت عمدهای از اکثر برنامهها کفایت میکنند؛ اما خیلی از ریزه کاریها وجود دارند که به این سادگیها قابل حل نیستند و در طی چند مقاله به آنها خواهیم پرداخت.
سؤال: در همین مثال فوق، اگر متن ورودی در TextBox تغییر کرد، چگونه میتوان بلافاصله از تغییرات آن در ViewModel مطلع شد؟ قدیمترها میشد نوشت:
<TextBox TextChanged="TextBox_TextChanged" />
اما الان که قرار نیست در code behind کد بنویسیم (تا حد امکان البته)، باید چکار کرد؟
پاسخ: امکان Binding به TextChanged وجود ندارد، پس آنرا فراموش میکنیم. اما همان Binding معمولی را به این صورت هم میشود نوشت (همان مثال قسمت قبل):
<TextBox Text="{Binding
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
و نکته مهم آن UpdateSourceTrigger است. اگر روی حالت پیش فرض باشد، ViewModel پس از تغییر focus از این TextBox به کنترلی دیگر، از تغییرات آگاه خواهد شد. اگر آنرا صریحا ذکر کرده و مساوی PropertyChanged قرار دهیم (این مورد در سیلورلایت 5 جدید است؛ هر چند از روز نخست WPF وجود داشته است)، با هر تغییری در محتوای TextBox، خاصیت MainPageModelData.Name به روز رسانی خواهد شد.
اگر هم بخواهیم این تغییرات آنیرا در ViewModel تحت نظر قرار دهیم، میتوان نوشت:
using System.ComponentModel;
namespace SL5Tests
{
public class MainPageViewModel
{
public MainPageModel MainPageModelData { set; get; }
public MainPageViewModel()
{
MainPageModelData = new MainPageModel();
MainPageModelData.Name = "Test1";
MainPageModelData.PropertyChanged += MainPageModelDataPropertyChanged;
}
void MainPageModelDataPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Name":
//do something
break;
}
}
}
}
تعریف MainPageModel را در قسمت قبل مشاهده کردهاید و این کلاس اینترفیس INotifyPropertyChanged را پیاده سازی میکند. بنابراین میتوان از رویدادگردان PropertyChanged آن در ViewModel هم استفاده کرد.
به این ترتیب همان کار رودیدادگردان TextChanged را اینطرف هم میتوان شبیه سازی کرد و تفاوتی نمیکند. البته با این تفاوت که در ViewModel فقط به اطلاعات به روز موجود در MainPageModelData.Name دسترسی داریم، اما نمیدانیم و نمیخواهیم هم بدانیم که منبع آن دقیقا کدام شیء رابط کاربری برنامه است.
سؤال: ما قبلا مثلا میتوانستیم بررسی کنیم که اگر کاربر حین تایپ در یک TextBox بر روی دکمهی Enter کلیک کرد، آنگاه برای نمونه، جستجویی بر اساس اطلاعات وارد شده صورت گیرد. الان این فشرده شدن دکمهی Enter را چگونه دریافت و چگونه به ViewModel ارسال کنیم؟
این مورد کمی پیشرفتهتر از حالتهای قبلی است. برای حل این مساله ابتدا باید UpdateSourceTrigger یاد شده را مساوی Explicit قرار داد. یعنی اینبار میخواهیم نحوه ی به روز رسانی خاصیت MainPageModelData.Name را از طریق Binding خودمان مدیریت کنیم. این مدیریت کردن هم با استفاده از امکاناتی به نام Attached properties قابل انجام است که به آنها Behaviors هم میگویند. مثلا:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace SL5Tests
{
public static class InputBindingsManager
{
public static readonly DependencyProperty UpdatePropertySourceWhenEnterPressedProperty
= DependencyProperty.RegisterAttached(
"UpdatePropertySourceWhenEnterPressed",
typeof(bool),
typeof(InputBindingsManager),
new PropertyMetadata(false, OnUpdatePropertySourceWhenEnterPressedPropertyChanged));
static InputBindingsManager()
{ }
public static void SetUpdatePropertySourceWhenEnterPressed(DependencyObject dp, bool value)
{
dp.SetValue(UpdatePropertySourceWhenEnterPressedProperty, value);
}
public static bool GetUpdatePropertySourceWhenEnterPressed(DependencyObject dp)
{
return (bool)dp.GetValue(UpdatePropertySourceWhenEnterPressedProperty);
}
private static void OnUpdatePropertySourceWhenEnterPressedPropertyChanged(DependencyObject dp,
DependencyPropertyChangedEventArgs e)
{
var txt = dp as TextBox;
if (txt == null)
return;
if ((bool)e.NewValue)
{
txt.KeyDown += HandlePreviewKeyDown;
}
else
{
txt.KeyDown -= HandlePreviewKeyDown;
}
}
static void HandlePreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;
var txt = sender as TextBox;
if (txt == null)
return;
var binding = txt.GetBindingExpression(TextBox.TextProperty);
if (binding == null) return;
binding.UpdateSource();
}
}
}
تعریف Attached properties یک قالب استاندارد دارد که آن را در کد فوق ملاحظه میکنید. یک تعریف به صورت static و سپس تعریف متدهای Get و Set آن. با تغییر مقدار آن که اینجا از نوع bool تعریف شده، متد OnUpdatePropertySourceWhenEnterPressedPropertyChanged به صورت خودکار فراخوانی میشود. اینجا است که ما از طریق آرگومان dp به textBox جاری دسترسی کاملی پیدا میکنیم. مثلا در اینجا بررسی شده که آیا کلید فشرده شده enter است یا خیر. اگر بله، یک سری فرامین را انجام بده. به عبارتی ما توانستیم، قطعه کدی را به درون شیءایی موجود تزریق کنیم. Txt تعریف شده در اینجا، واقعا همان کنترل TextBox ایی است که به آن متصل شدهایم.
و برای استفاده از آن خواهیم داشت:
<UserControl x:Class="SL5Tests.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:VM="clr-namespace:SL5Tests"
mc:Ignorable="d" Language="fa"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<VM:MainPageViewModel x:Name="vmMainPageViewModel" />
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainPageViewModel}}"
x:Name="LayoutRoot"
Background="White">
<TextBox Text="{Binding
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
VerticalAlignment="Top"
VM:InputBindingsManager.UpdatePropertySourceWhenEnterPressed="True" />
</Grid>
</UserControl>
همانطور که مشاهده میکنید، UpdateSourceTrigger به Explicit تنظیم شده و سپس InputBindingsManager.UpdatePropertySourceWhenEnterPressed به این کنترل متصل گردیده است. یعنی تنها زمانیکه در متد HandlePreviewKeyDown ذکر شده، متد UpdateSource فراخوانی گردد، خاصیت MainPageModelData.Name به روز رسانی خواهد شد (کنترل آنرا خودمان در دست گرفتهایم نه حالتهای از پیش تعریف شده).
این روش، روش متداولی است برای تبدیل اکثر حالاتی که Binding و Commanding متداول در مورد آنها وجود ندارد. مثلا نیاز است focus را به آخرین سطر یک ListView از داخل ViewModel انتقال داد. در حالت متداول چنین امری میسر نیست، اما با تعریف یک Attached properties میتوان به امکانات شیء ListView مورد نظر دسترسی یافت (به آن متصل شد، یا نوعی تزریق)، آخرین عنصر آنرا یافته و سپس focus را به آن منتقل کرد یا به هر اندیسی مشخص که بعدا در ViewModel به این Behavior از طریق Binding ارسال خواهد شد.
فارسی نویسی و iTextSharp
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 };
ممنون میشم راهنمایی کنید
مرسی
عدم سازگاری با EF
وقتی دیتا سورس از جنس اینتیتی تعریف میکنم بحتی هیچ ستونی هم اد نمیکنم بدون اینکه خطایی دریافت کنم فایل پی دی اف، read only میمونه و حجم فایل 0kb باقی میمونه
public static IPdfReportData CreatePdfReport(Order order, int languageId) { if (order == null) throw new ArgumentNullException("order"); Language lang = IoC.Resolve<ILanguageService>().GetLanguageById(languageId); if (lang == null) throw new NopException("Language could not be loaded"); var localizationManager = IoC.Resolve<ILocalizationManager>(); var orderProductVariants = order.OrderProductVariants; return new PdfReport().DocumentPreferences(doc => { doc.RunDirection(PdfRunDirection.LeftToRight); doc.Orientation(PageOrientation.Portrait); doc.PageSize(PdfPageSize.A4); doc.DocumentMetadata(new DocumentMetadata { Author = "coponet", Application = "coponet eshop", Keywords = "Factor", Subject = "Factor", Title = "Factor" }); doc.Compression(new CompressionSettings { EnableCompression = true, EnableFullCompression = true }); doc.PrintingPreferences(new PrintingPreferences { ShowPrintDialogAutomatically = false }); }) .DefaultFonts(fonts => { fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\arial.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(CalenderHelper.dateTimeParseToString("yyyy/MM/dd", order.CreatedOn, CalenderEnum.PersianCalender)); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.RunDirection(PdfRunDirection.LeftToRight); //defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("Our new rpt."); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.NumberOfDataRowsPerPage(1); }) .MainTableDataSource(dataSource => { //var listOfRows = new List<User>(); //for (int i = 0; i < 40; i++) //{ // listOfRows.Add(new User {Id = i}); //} //dataSource.StronglyTypedList(listOfRows); dataSource.StronglyTypedList(orderProductVariants); }) ) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "There is no data available to display."); }) ).Generate(data => data.AsPdfFile(string.Format("{0}\\documents\\Temp\\Factor-{1}.pdf", HttpRuntime.AppDomainAppPath, order.OrderId))); }
این نمونه کدی که استفاده کردم
یک نکتهی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئریهای خود ارجاعی
همانطور که در مطلب «مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجهی آن، بسیار ساده میشود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسیهای ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت میتوان به صورت زیر عمل کرد:
الف) تشکیل یک کلاس پایه برای تعریف سادهتر و مشخص رابطههای خود ارجاعی
public abstract class BaseEntity { public int Id { get; set; } } public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity where TSelfEntity : BaseEntity { public virtual TSelfEntity? Reply { set; get; } public int? ReplyId { get; set; } public virtual ICollection<TSelfEntity>? Children { get; set; } }
که ساختار معرفی شدهی در اینجا، با توضیحات موجود در متن، انطباق دارد.
ب) پر کردن درخت نهایی حاصل به صورت دستی:
چون دیگر EF این درخت را برای ما تشکیل نمیدهد، اکنون باید خودمان کار تشکیل آنرا به صورت زیر انجام دهیم:
public static class SelfReferencingExtensions { public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList) where TEntity : BaseSelfReferencingEntity<TEntity> { var results = new List<TEntity>(); if (originalList is null || originalList.Count == 0) { return results; } foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue)) { results.Add(rootItem); AppendChildren(originalList, rootItem); } return results; } private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem) where TEntity : BaseSelfReferencingEntity<TEntity> { foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id)) { parentItem.Children ??= new List<TEntity>(); parentItem.Children.Add(kid); AppendChildren(originalList, kid); } } }
در اینجا کار تشکیل درخت نهایی، با استفاده از یک متد بازگشتی، انجام میشود.
پس از این مقدمات، نحوهی استفاده از آن به صورت زیر است:
var comments = await _comments.AsNoTracking() .Where(x => x.ParentId == postId) .OrderBy(x => x.Id) .Take(count) .ToListAsync(); var commentsTree = comments.ToSelfReferencingTree();
کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکتهی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده میشوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخها را به صورت خودکار انجام میدهد.
[Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching", }; // /WeatherForecast/GetForecast2?from=1&to=3 [HttpGet("[action]")] public IEnumerable<WeatherForecast> GetForecast2(Duration days) { return Enumerable.Range(days.From, days.To) .Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)], }) .ToArray(); } }
public class Duration { public int From { get; set; } public int To { get; set; } } public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string? Summary { get; set; } }
روش دیگر پردازش اطلاعات رشتهای رسیده و تشکیل یک Model Binder سفارشی در ASP.NET Core 7x
اکنون فرض کنید بجای آدرس WeatherForecast/GetForecast2?from=1&to=3 که اطلاعات را از طریق کوئری استرینگ مشخص و استانداردی دریافت میکند، میخواهیم اطلاعات آنرا از طریق یک قالب سفارشی و غیراستاندارد مانند WeatherForecast/GetForecast3/1-3 دریافت کنیم:
// /WeatherForecast/GetForecast3/1-3 [HttpGet("[action]/{days}")] public IEnumerable<WeatherForecast> GetForecast3(Days days) { return Enumerable.Range(days.From, days.To) .Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)], }) .ToArray(); }
using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace NET7Mvc.Models; public class Days : IParsable<Days> { public Days(int from, int to) { From = from; To = to; } public int From { get; } public int To { get; } public static bool TryParse([NotNullWhen(true)] string? value, IFormatProvider? provider, [MaybeNullWhen(false)] out Days result) { var items = value?.Split('-', StringSplitOptions.RemoveEmptyEntries); if (items is { Length: 2 }) { if (int.TryParse(items[0], NumberStyles.None, provider, out var from) && int.TryParse(items[1], NumberStyles.None, provider, out var to)) { result = new Days(from, to); return true; } } result = default; return false; } public static Days Parse(string value, IFormatProvider? provider) { if (!TryParse(value, provider, out var result)) { throw new ArgumentException("Could not parse the given value.", nameof(value)); } return result; } }
- همینقدر که مدلی IParsable را پیاده سازی کرده باشد، از امکانات آن به صورت خودکار استفاده خواهد شد و نیازی به معرفی و یا تنظیمات خاص دیگری ندارد.
- البته این قابلیت جدید نیست و پشتیبانی از IParsable، پیشتر در Minimal API دات نت 6 ارائه شده بود؛ اما در دات نت 7 توسط ASP.NET Core MVC نیز قابل استفاده شدهاست.
پس از آن، برای تبدیل صفحات یک فایل PDF به تصویر، مراحل زیر باید طی شود:
الف) وهله سازی از شیء AcroExch.PDDoc
در صورتیکه SDK یاد شده بر روی سیستم نصب نباشد، این وهله سازی با شکست مواجه خواهد شد و همچنین باید دقت داشت که این SDK به همراه نگارش رایگان Adobe reader ارائه نمیشود.
ب) گشودن فایل PDF به کمک شیء Com وهله سازی شده (pdfDoc.Open)
ج) دریافت اطلاعات صفحه مورد نظر (pdfDoc.AcquirePage)
د) کپی این اطلاعات به درون clipboard ویندوز (pdfPage.CopyToClipboard)
به این ترتیب به یک تصویر Bmp قرار گرفته شده در clipboard ویندوز خواهیم رسید
ه) مرحله بعد تغییر ابعاد و ذخیره سازی این تصویر نهایی است.
کدهای زیر، روش انجام این مراحل را بیان میکنند:
using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; using Acrobat; //Add a Com Object ref. to "Adobe Acrobat 10.0 Type Library" => Program Files\Adobe\Acrobat 10.0\Acrobat\acrobat.tlb using Microsoft.Win32; namespace PdfThumbnail.Lib { public static class PdfToImage { const string AdobeObjectsErrorMessage = "Failed to create the PDF object."; const string BadFileErrorMessage = "Failed to open the PDF file."; const string ClipboardError = "Failed to get the image from clipboard."; const string SdkError = "This operation needs the Acrobat SDK(http://www.adobe.com/devnet/acrobat/downloads.html), which is combined with the full version of Adobe Acrobat."; public static byte[] PdfPageToPng(string pdfFilePath, int thumbWidth = 600, int thumbHeight = 750, int pageNumber = 0) { byte[] imageData = null; runJob((pdfDoc, pdfRect) => { imageData = pdfPageToPng(thumbWidth, thumbHeight, pageNumber, pdfDoc, pdfRect); }, pdfFilePath); return imageData; } public static void AllPdfPagesToPng(Action<byte[], int, int> dataCallback, string pdfFilePath, int thumbWidth = 600, int thumbHeight = 750) { runJob((pdfDoc, pdfRect) => { var numPages = pdfDoc.GetNumPages(); for (var pageNumber = 0; pageNumber < numPages; pageNumber++) { var imageData = pdfPageToPng(thumbWidth, thumbHeight, pageNumber, pdfDoc, pdfRect); dataCallback(imageData, pageNumber + 1, numPages); } }, pdfFilePath); } static void runJob(Action<CAcroPDDoc, CAcroRect> job, string pdfFilePath) { if (!File.Exists(pdfFilePath)) throw new InvalidOperationException(BadFileErrorMessage); var acrobatPdfDocType = Type.GetTypeFromProgID("AcroExch.PDDoc"); if (acrobatPdfDocType == null || !isAdobeSdkInstalled) throw new InvalidOperationException(SdkError); var pdfDoc = (CAcroPDDoc)Activator.CreateInstance(acrobatPdfDocType); if (pdfDoc == null) throw new InvalidOperationException(AdobeObjectsErrorMessage); var acrobatPdfRectType = Type.GetTypeFromProgID("AcroExch.Rect"); var pdfRect = (CAcroRect)Activator.CreateInstance(acrobatPdfRectType); var result = pdfDoc.Open(pdfFilePath); if (!result) throw new InvalidOperationException(BadFileErrorMessage); job(pdfDoc, pdfRect); releaseComObjects(pdfDoc, pdfRect); } public static byte[] ResizeImage(this Image image, int thumbWidth, int thumbHeight) { var srcWidth = image.Width; var srcHeight = image.Height; using (var bmp = new Bitmap(thumbWidth, thumbHeight, PixelFormat.Format32bppArgb)) { using (var gr = Graphics.FromImage(bmp)) { gr.SmoothingMode = SmoothingMode.HighQuality; gr.PixelOffsetMode = PixelOffsetMode.HighQuality; gr.CompositingQuality = CompositingQuality.HighQuality; gr.InterpolationMode = InterpolationMode.High; var rectDestination = new Rectangle(0, 0, thumbWidth, thumbHeight); gr.DrawImage(image, rectDestination, 0, 0, srcWidth, srcHeight, GraphicsUnit.Pixel); using (var memStream = new MemoryStream()) { bmp.Save(memStream, ImageFormat.Png); return memStream.ToArray(); } } } } static bool isAdobeSdkInstalled { get { return Registry.ClassesRoot.OpenSubKey("AcroExch.PDDoc", writable: false) != null; } } private static Bitmap pdfPageToBitmap(int pageNumber, CAcroPDDoc pdfDoc, CAcroRect pdfRect) { var pdfPage = (CAcroPDPage)pdfDoc.AcquirePage(pageNumber); if (pdfPage == null) throw new InvalidOperationException(BadFileErrorMessage); var pdfPoint = (CAcroPoint)pdfPage.GetSize(); pdfRect.Left = 0; pdfRect.right = pdfPoint.x; pdfRect.Top = 0; pdfRect.bottom = pdfPoint.y; pdfPage.CopyToClipboard(pdfRect, 0, 0, 100); Bitmap pdfBitmap = null; var thread = new Thread(() => { var data = Clipboard.GetDataObject(); if (data != null && data.GetDataPresent(DataFormats.Bitmap)) pdfBitmap = (Bitmap)data.GetData(DataFormats.Bitmap); }); thread.SetApartmentState(ApartmentState.STA); thread.Start(); thread.Join(); Marshal.ReleaseComObject(pdfPage); return pdfBitmap; } private static byte[] pdfPageToPng(int thumbWidth, int thumbHeight, int pageNumber, CAcroPDDoc pdfDoc, CAcroRect pdfRect) { var pdfBitmap = pdfPageToBitmap(pageNumber, pdfDoc, pdfRect); if (pdfBitmap == null) throw new InvalidOperationException(ClipboardError); var pdfImage = pdfBitmap.GetThumbnailImage(thumbWidth, thumbHeight, null, IntPtr.Zero); // (+ 7 for template border) var imageData = pdfImage.ResizeImage(thumbWidth + 7, thumbHeight + 7); return imageData; } private static void releaseComObjects(CAcroPDDoc pdfDoc, CAcroRect pdfRect) { pdfDoc.Close(); Marshal.ReleaseComObject(pdfRect); Marshal.ReleaseComObject(pdfDoc); } } }
using System; using System.IO; using System.Windows.Forms; using PdfThumbnail.Lib; namespace PdfThumbnail { class Program { static void Main(string[] args) { var pdfPath = Application.StartupPath + @"\test.pdf"; PdfToImage.AllPdfPagesToPng((pageImageData, pageNumber, numPages) => { Console.WriteLine("Page {0}/{1}", pageNumber, numPages); File.WriteAllBytes(string.Format("{0}\\page-{1}.png", Application.StartupPath, pageNumber), pageImageData); }, pdfPath); } } }
کدهای این قسمت را از اینجا نیز میتوانید دریافت کنید:
PdfThumbnail.zip
برای بررسی ویژگیهای جاوا اسکریپت مدرن، یک پروژهی جدید React را ایجاد میکنیم.
> create-react-app sample-02 > cd sample-02 > npm start
به علاوه چون در این قسمت خروجی UI نخواهیم داشت، تمام خروجی را در کنسول developer tools مرورگر خود میتوانید مشاهده کنید (فشردن دکمهی F12).
var، let و const
در اکثر زبانهای برنامه نویسی، متغیرها در محدودهی دید قطعه کدی که تعریف شدهاند (scope)، قابل دسترسی هستند. برای نمونه محتوای فایل index.js پروژه را به صورت زیر تغییر داده و با فرض اجرای دستور npm start، خروجی آنرا میتوان در کنسول مرورگر مشاهده کرد.
function sayHello() { for (var i = 0; i < 5; i++) { console.log(i); } console.log(i); } sayHello();
در آخرین پیمایش حلقه، i مساوی 5 شده و از حلقه خارج میشود. اما چون در اینجا برای تعریف متغیر از واژهی کلیدی var استفاده شدهاست، محدودهی دید آن به کل تابعی که در آن تعریف شدهاست، بسط پیدا میکند. به همین جهت در این خروجی، عدد 5 را نیز مشاهده میکند که حاصل دسترسی به i، خارج از حلقهاست.
برای یک دست سازی این رفتار با سایر زبانهای برنامه نویسی، در ES6، واژهی کلیدی جدیدی به نام let تعریف شدهاست که میدان دید متغیر را به قطعه کدی که در آن تعریف شدهاست، محدود میکند. اکنون اگر در حلقهی فوق بجای var از let استفاده شود، یک چنین خطایی در مرورگر ظاهر خواهد شد که عنوان میکند، i استفاده شدهی در خارج از حلقه، تعریف نشدهاست.
./src/index.js Line 14:15: 'i' is not defined no-undef Search for the keywords to learn more about each error.
علاوه بر let، واژهی کلیدی جدید const نیز به ES6 اضافه شدهاست که از آن برای تعریف ثوابت استفاده میشود. constها نیز همانند let، میدان دید محدود شدهای به قطعه کد تعریف شدهی در آن دارند؛ اما قابلیت انتساب مجدد را ندارند:
const x = 1; x = 2; // Attempting to override 'x' which is a constant.
به صورت خلاصه از این پس واژهی کلیدی var را فراموش کنید. همیشه با const جهت تعریف متغیرها شروع کنید. اگر به خطا برخوردید و نیاز به انتساب مجدد وجود داشت، آنرا به let تغییر دهید. بنابراین استفاده از const همیشه نسبت به let ارجحیت دارد.
اشیاء در جاوا اسکریپت
اشیاء در جاوا اسکریپت به صورت مجموعهای از key/valueها تعریف میشوند:
const person = { name: "User 1", walk: function() {}, // method talk() {} // concise method };
const person = { name: "User 1", walk() {}, talk() {} };
person.talk(); person.name = "User 3"; person["name"] = "User 2";
مورد آخر همان روش استفاده از key/valueها است که اساس اشیاء جاوا اسکریپتی را تشکیل میدهد. البته از این روش فقط زمانی استفاده کنید که قرار است یکسری کار پویا صورت گیرند (مقدار key به صورت متغیر دریافت شود) و از ابتدا مشخص نیست که کدام خاصیت یا متد قرار است تعریف و استفاده شود:
const targetMember = "name"; person[targetMember] = "User 2";
واژهی کلیدی this در جاوا اسکریپت
از واژهی کلیدی this، در قسمتهای بعدی زیاد استفاده خواهیم کرد. به همین جهت نیاز است تفاوتهای اساسی آنرا با سایر زبانهای برنامه نویسی بررسی کنیم.
همان شیء person را که پیشتر تعریف کردیم درنظر بگیرید. در متد walk آن، مقدار this را لاگ میکنیم:
const person = { name: "User 1", walk() { console.log(this); }, talk() {} }; person.walk();
شیء this در جاوا اسکریپت، همانند سایر زبانهای برنامه نویسی مانند سیشارپ و یا جاوا رفتار نمیکند. در سایر زبانهای نامبرده شده، this همواره ارجاعی را به وهلهای از شیء جاری، باز میگرداند؛ دقیقا همانند تصویری که در بالا مشاهده میکنید. در اینجا نیز this جاوا اسکریپتی لاگ شده، ارجاعی را به وهلهی جاری شیء person، بازگشت دادهاست. اما مشکل اینجا است که this در جاوا اسکریپت، همیشه به این صورت رفتار نمیکند!
برای نمونه در ادامه یک ثابت را به نام walk تعریف کرده و آنرا به person.walk مقدار دهی میکنیم:
const walk = person.walk; console.log(walk);
سؤال: اکنون اگر این function را با فراخوانی ()walk اجرا کنیم، چه خروجی را میتوان مشاهده کرد؟
اینبار this لاگ شده، به شیء person اشاره نمیکند و شیء استاندارد window مرورگر را بازگشت دادهاست!
اگر یک function به صورت متدی از یک شیء فراخوانی شود، مقدار this همواره اشارهگری به وهلهای از آن شیء خواهد بود. اما اگر این تابع به صورت متکی به خود و به صورت یک function و نه متد یک شیء، فراخوانی شود، اینبار this، شیء سراسری جاوا اسکریپت یا همان شیء window را بازگشت میدهد.
یک نکته: اگر strict mode جاوا اسکریپت را در پروژهی جاری فعال کنیم، بجای شیء window، مقدار undefined را در خروجی فوق شاهد خواهیم بود.
اتصال مجدد this به شیء اصلی در جاوا اسکریپت
تا اینجا دریافتیم که اگر یک function را به صورت متکی به خود و نه جزئی از یک شیء فراخوانی کنیم، شیء this در این حالت به شیء window سراسری مرورگر اشاره میکند و اگر strict mode فعال باشد، فقط undefined را بازگشت میهد. اکنون میخواهیم بررسی کنیم که چگونه میتوان این مشکل را برطرف کرد؛ یعنی صرفنظر از نحوهی فراخوانی متدها یا تابعها، this همواره ارجاعی را به شیء person بازگشت دهد.
در جاوا اسکریپت، تابعها نیز شیء هستند. برای مثال person.walk نوشته شده نیز یک شیء است. برای اثبات سادهی آن فقط یک دات را پس از person.walk قرار دهید:
همانطور که مشاهده میکنید، شیء person.walk مانند تمام اشیاء دیگر جاوا اسکریپت، به همراه متد bind نیز هست. کار آن، انقیاد یک تابع، به یک شیء است. یعنی هرچیزی را که به عنوان آرگومان آن، به آن ارسال کنیم، به عنوان مقدار شیء this درنظر میگیرد:
const walk2 = person.walk.bind(person); console.log(walk2); walk2();
Arrow functions
تابع زیر را درنظر بگیرید که به یک ثابت انتساب داده شدهاست:
const square = function(number) { return number * number; };
const square2 = (number) => { return number * number; };
const square2 = number => { return number * number; };
در ادامه اگر بدنهی این تابع، فقط حاوی یک return بود، میتوان آنرا به صورت زیر نیز خلاصه کرد (در اینجا {} به همراه واژهی کلیدی return حذف میشوند):
const square3 = number => number * number; console.log(square3(5));
اکنون مثال مفید دیگری را در مورد Arrow functions بررسی میکنیم که بیشتر شبیه به عبارات LINQ در #C است:
const jobs = [ { id: 1, isActive: true }, { id: 2, isActive: true }, { id: 3, isActive: true }, { id: 4, isActive: true }, { id: 5, isActive: false } ];
var activeJobs = jobs.filter(function(job) { return job.isActive; });
در ادامه میتوان این تابع را توسط arrow functions به صورت سادهتر زیر نیز نوشت:
var activeJobs2 = jobs.filter(job => job.isActive);
ارتباط بین arrow functions و شیء this
نکتهی مهمی را که باید در مورد arrow functions دانست این است که شیء this را rebind نمیکنند (rebind: مقدار دهی مجدد؛ ریست کردن).
در مثال زیر، ابتدا شیء user با متد talk که در آن شیء this، لاگ شده، ایجاد شده و سپس این متد فراخوانی گردیدهاست:
const user = { name: "User 1", talk() { console.log(this); } }; user.talk();
اکنون اگر متد لاگ کردن را داخل یک تایمر قرار دهیم چه اتفاقی رخ میدهد؟
const user = { name: "User 1", talk() { setTimeout(function() { console.log(this); }, 1000); } }; user.talk();
در این حالت خروجی console.log، مجددا همان شیء سراسری window مرورگر است و دیگر به وهلهی جاری شیء user اشاره نمیکند. علت اینجا است که پارامتر اول متد setTimeout که یک callback function نام دارد، جزئی از هیچ شیءای نیست. بنابراین دیگر مانند فراخوانی متد ()user.talk در مثال قبلی کار نمیکند؛ چون متکی به خود است. هر زمان که یک متد متکی به خود غیر وابستهی به یک شیء را اجرا کنیم، به صورت پیشفرض this آن، به شیء window مرورگر اشاره میکند.
سؤال: چگونه میتوان درون یک callback function متکی به خود، به this همان شیء user جاری دسترسی یافت؟
یک روش حل این مساله، ذخیره this شیء user در یک متغیر و سپس ارسال آن به متد متکی به خود setTimeout است:
const user2 = { name: "User 2", talk() { var self = this; setTimeout(function() { console.log(self); }, 1000); } }; user2.talk();
const user3 = { name: "User 3", talk() { setTimeout(() => console.log(this), 1000); } }; user3.talk();
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-02.zip
در قسمت بعد نیز بررسی پیشنیازهای جاوا اسکریپتی شروع به کار با React را ادامه خواهیم داد.
ASP.NET MVC #8
جایی که وهلهای از اشیاء به View متناظر ارسال میشود در اکشن متد مشخص شده است (return View):
public ActionResult Index() { var products = new Products(); return View(products); }
رفع مشکلات:
در قسمت قبل با ذکر یک مثال و بیان مشکلات آن از دیدگاه اصول Defensive Code قصد داشتیم که مساله را روشنتر کنیم. مواردی که در
قسمت قبل ذکر شدند، به سادهترین شکل ممکن بیان
شدند و شما به راحتی با بررسی این موارد و تفکر در کدهای خود، میتوانید
این موارد را در کدی که خودتان مینویسید رعایت کنید. حل پیچیدگیهای موجود در کد قبل، با در نظر گرفتن اصول مذکور و اصولهای طراحی مختلف میتواند به روشهای مختلفی انجام گیرد. برای مثال میتوان برای هر یک از کارهایی که کد مثال قبل انجام میدهد، یک کلاس مجزا ایجاد نمود و اصول مذکور را در آن رعایت کرد. درنهایت این
کلاسها را در قالب یک Class Library دسته بندی کرد.
Predictability:
در ادامه قصد داریم در مورد قابلیت پیش بینی و فواید رعایت اصول آن در کد، بحث کنیم. به صورت کلی یک متد، یکسری پارامترها را به عنوان ورودی دریافت میکند؛ یک عملیات خاص را بر روی پارامترهای ورودی انجام میدهد و در نهایت، در صورت لزوم یک مقدار را بر میگرداند. پارامترهای ورودی میتوانند از سمت کاربر و از یک سورس خارجی وارد شوند و یا میتوانند از یک متد دیگر به این متد ارسال شوند. اولین مرحله برای ایجاد قابلیت Predictability این است که یکسری گاردها را به کد خود اضافه کنید تا به وسیلهی آنها پارامترهای ورودی را بررسی کنید و فقط اجازهی ورود، ورودیهای معتبر را بدهیم. برای مثال کد ذیل را درنظر بگیرد.
پارامترهای ورودی این کد با مستطیل قرمز رنگ مشخص شده اند. حال ما سعی داریم با قرار دادن یکسری گارد برای پارامترهای ورودی، از ورود مقادیر ناخواسته جلوگیری کنیم.
بر اساس اصول (GIGO (Garbage in-Garbage out در برنامه نویسی متدی که ورودیهای نامعتبر به آن پاس داده شوند، خروجیهای نامعتبری هم پس خواهد داد. بنابراین برای جلوگیری از این مسئله باید از ورود ورودیهای نامعتبر به متدها جلوگیری کرد. گاردها از ورود مقادیر نامعتبر به متدها جلوگیری خواهند کرد و در نتیجه خروجی مناسب و قابل پیش بینی از متد گرفته خواهد شد. برای جلوگیری از ورود دادههای نامعتبر، باید با استفاده از این دستورات که در ابتدای متد قرار داده میشوند، از ورود دادههای نامعتبر جلوگیری کرد. به این دستورات Guard Clauses گفته میشود. غیر از این مساله، کاهش دادن تعداد پارامترها و قراردادن قانونی برای تعیین اولویت پارامترهای متدها (برای مثال با توجه به اهمیت) میتواند به افزایش Predictability متدها بسیار کمک کند. با پیروی کردن از این اصول ساده شما میتوانید میزان خطاهایی که از پارامترهای ورودی منشاء میگیرند را کاهش دهید.
اجازه دهید با یک مثال؛ مسالهی بالا را تشریح کنیم. برای مثال یک برنامهی کوچک نوشتهایم؛ برای شمردن گام ها. در این برنامه تعداد قدمهای هدف و تعداد قدمهای برداشته شدهی امروز تعیین میشوند و سپس هدف، بر حسب درصد بیان خواهد شد.
با استفاده از این Application میخواهیم مفاهیمی را که بیان کردیم، به صورت کاربردی
نمایش دهیم. کدی این محاسبه را برای ما انجام میدهد، در ذیل نمایش داده شده و
در قالب یک متد تعیین شده است.
private decimal CalculatePercentOfGoalSteps (string goalSteps, string actualSteps) { return (Convert.ToDecimal(actualSteps) / Convert.ToDecimal(goalSteps)) * 100; }
این متد دارای دو پارامتر از نوع string می باشد و نتیجه هم در قالب یک مقدار decimal بازگشت داده خواهد شد. این جمله کلیتی از متد را بیان خواهد کرد. نحوهی فراخوانی این متد هم در کد ذیل آورده شد است.
private void Calculate_Click(object sender, EventArgs e) { var result =CalculatePercentOfGoalSteps (stepGoalForTodayTxt.Text, numberOfStepsForToday.Text); lblResult.Text = "شما به" + result + "% از هدف تان رسیده اید"; }
حال Application را اجرا کرده و نتیجه کار را مشاهده میکنیم. برای مثال شکل ذیل:
در این مثال با توجه به مقادیر وارد شده، به 40 درصد از هدف مورد نظر رسیدهایم. اما هدف از بیان این مثال، این نیست که مشخص گردد که ما چقدر به هدفمان نزدیک شدهایم. بلکه هدف مسایل دیگری است. در نظر بگیرید که بجای 5000، صفر را وارد کنید. در این حالت با یک Exception روبرو میشویم:
همانطور که در شکل بالا مشاهده میکنید، خطای Divide by zero رخ داده است. برای رفع این خطا و جلوگیری از رخداد این خطا، میتوان کد
ذیل را پیشنهاد داد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal result =0; var goalStepsCount = Convert.ToDecimal(goalSteps); if (goalStepsCount>0) { result = (Convert.ToDecimal(actualSteps) / goalStepsCount) * 100; } return result; }
با تغییر کد به این صورت مشکل Exception بالا حل میشود، اما باز هم مشکل دیگری وجود دارد. فرض کنید همانند شکل ذیل textbox اول را خالی کنیم و بعد از آن سعی در محاسبه داشته باشیم،
باز هم یک Exception دیگر
علت بوجود آمدن این مشکل این است که ما در کد امکان خالی بودن پارامترهای متد را در نظر نگرفتهایم و پیش بینیهای لازم صورت نگرفته است بنابراین دستور Convert .با مشکل مواجه شد. برای حل این مشکل میتوان به جای Convert از decimal.Tryparse استفاده کرد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal result = 0; decimal goalStepsCount = 0; decimal.TryParse(goalSteps, out goalStepsCount); decimal actualStepsCount = 0; decimal.TryParse(actualSteps, out actualStepsCount); if (goalStepsCount>0) { result = (actualStepsCount / goalStepsCount) * 100; } return result; }
با انجام دادن این کارها از بروز خطاهایی که ناشی از ورودیهای نامعتبر در کد هستند، جلوگیری کردیم. اما آیا این پایان کار است؟ خیر با استفاده کردن از این روش ما توانستهایم که از بروز خطا در برنامه جلوگیری کنیم. اما مشکلی که این روش دارد این است که کاربر متوجه نمیشود که چه زمانی برنامه دچار مشکل شده است. کاری که ما انجام میدهیم این است که برای تمامی حالات خطا، مقدار صفر را بر میگردانیم.
برای اینکه بتوانیم این کد به راحتی debug کنیم باید از مفهوم Fail Fast استفاده کنیم . این مفهوم قابلیتی را در کد ایجاد میکند که در صورتی که کد، دادههای نامعتبری را دریافت کرد، سریعا اجرای آن متوقف میشود و همزمان نیز اطلاعاتی در مورد خطا در اختیار کاربر قرار میدهد. برای این منظور با قرار دادن یکسری Guard Clauses، کد بالا را همانند شکل ذیل تغییر خواهیم داد.
private decimal CalculatePercentOfGoalSteps(string goalSteps, string actualSteps) { decimal goalStepsCount = 0; decimal actualStepsCount = 0; /// اطمینان حاصل میکنند که پارامترهای ورودی دارای مقدار هستند if (string.IsNullOrWhiteSpace(goalSteps)) throw new ArgumentException("مقدار هدف باید وارد شود", "goalSteps"); if (string.IsNullOrWhiteSpace(actualSteps)) throw new ArgumentException("مقدار واقعی باید وارد شود", "goalSteps"); ///اطمینان حاصل میکنند که مقادیر وارد شده حتما عددی هستند if (!decimal.TryParse(goalSteps, out goalStepsCount)) throw new ArgumentException("مقدار هدف باید عددی باشد", goalSteps); if(!decimal.TryParse(actualSteps, out actualStepsCount)) throw new ArgumentException("مقدار واقعی باید عددی باشد", actualSteps); ///اطمینان حاصل میکند که مقدار متغیر نباید صفر باشد if (goalStepsCount <= 0) throw new ArgumentException("مقدار هدف نباید صفر و یا کمتر از صفر باشد", "goalStepsCount"); return (actualStepsCount / goalStepsCount) * 100; }
ایجاد کردن این تغییرات در متد باعث افزایش خوانایی
کد میشود و هدف متد را روشنتر بیان خواهد کرد. اضافه کردن این کدها به دلیل اینکه
تمامی شرایط تست را تعیین خواهیم کرد Test-ability کد را بالا میبرد. اضافه کردن کدهای بالا به برنامه کمک خواهد کرد که
شرایط خطا در برنامه به درستی هندل شود و به طبع آن تصمیمات مناسبی گرفته شود و در
نهایت Predictability متدها و کل برنامه را افزایش میهد.