در مورد static reflection مقدمهای پیشتر در این سایت قابل مطالعه است (^) و پیشنیاز بحث جاری است. در ادامه قصد داریم یک سری از کاربردهای متداول آنرا که این روزها در گوشه و کنار وب یافت میشود، به زبان ساده بررسی کنیم.
بهبود کدهای موجود
از static reflection در دو حالت کلی میتوان استفاده کرد. یا قرار است کتابخانهای را از صفر طراحی کنیم یا اینکه خیر؛ کتابخانهای موجود است و میخواهیم کیفیت آنرا بهبود ببخشیم. هدف اصلی هم «حذف رشتهها» و «استفاده از کد بجای رشتهها» است.
برای مثال قطعه کد زیر یک مثال متداول مرتبط با WPF و یا Silverlight است. در آن با پیاده سازی اینترفیس INotifyPropertyChanged و استفاده از متد raisePropertyChanged ، به رابط کاربری برنامه اعلام خواهیم کرد که لطفا خودت را بر اساس اطلاعات جدید تنظیم شده در قسمت set خاصیت Name ، به روز کن:
using System.ComponentModel;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
تعاریف قسمت PropertyChangedEventArgs این پیاده سازی، خارج از کنترل ما است و در دات نت فریم ورک تعریف شده است. حتما هم نیاز به رشته دارد؛ آن هم نام خاصیتی که تغییر کرده است. چقدر خوب میشد اگر میتوانستیم این رشته را حذف کنیم تا کامپایلر بتواند صحت بکارگیری اطلاعات وارد شده را دقیقا پیش از اجرای برنامه بررسی کند. الان فقط در زمان اجرا است که متوجه خواهیم شد، مثلا آیا به روز رسانی مورد نظر صورت گرفتهاست یا خیر؛ اگر نه، یعنی احتمالا یک اشتباه تایپی جایی وجود دارد.
برای بهبود این کد همانطور که در قسمت قبل نیز گفته شد، از ترکیب کلاسهای Expression و Func استفاده خواهیم کرد. در اینجا Func قرار نیست چیزی را اجرا کند، بلکه از آن به عنوان قطعه کدی که اطلاعاتش قرار است استخراج شود (Lambdas as Data) استفاده میشود. این استخراج اطلاعات هم توسط کلاس Expression انجام میشود. بنابراین قسمت اول بهبود کد به صورت زیر شروع میشود:
void raisePropertyChanged(Expression<Func<object>> expression)
الان اگر متد raisePropertyChanged بکارگرفته شده در خاصیت Name را بخواهیم اصلاح کنیم، حداقل با دو واقعهی مطلوب زیر مواجه خواهیم شد:
Intellisense به صورت خودکار کار میکند:
حتی بدویترین ابزارهای Refactoring موجود (منظور همان ابزار توکار VS.NET است!) هم امکان Refactoring را در اینجا فراهم خواهند ساخت:
در پایان کد تکمیل شده فوق به شرح زیر خواهد بود که در آن از کلاس Expression جهت استخراج Member.Name استفاده شده است:
using System;
using System.ComponentModel;
using System.Linq.Expressions;
namespace StaticReflection
{
public class User : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
if (_name == value) return;
_name = value;
raisePropertyChanged(() => Name);
}
}
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(Expression<Func<object>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
throw new InvalidOperationException("Not a member access.");
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
}
}
}
برنامه نویسانی که میخواهند رابط کاربری و محتوای جالبی بسازند، Windows Presentation Foundation (WPF) از چند رسانهای ، گرافیک برداری، انیمیشن و ترکیبی از آنها پشتیبانی میکند. با استفاده از Microsoft Visual Studio میتوانید یک گرافیک برداری یا انیمیشن پیچیده و درج مدیا را در داخل برنامه داشته باشید.
این مبحث ویژگیهای گرافیکی، انیمیشن و مدیای WPF را معرفی میکند و شما را برای اضافه کردن گرافیک، افکت، صدا و ویدئو در داخل برنامهاتان قادر میسازد.
موارد جدیدی که با گرافیک و چند رسانهای در WPF 4 مطرح شدهاند:
چندین تغییری که مرتبط با انیمیشن و گرافیک میباشد اتفاق افتاده است.
- Layout Rounding :
این مثال تاثیر استفاده از UseLayoutRounding را شرح میدهد. اگر شما در این مثال به آرامی اندازه پنجره را تغییر دهید، تفاوت دو شیء رسم شده را کاملا درک خواهید کرد.
<Page x:Class="LayoutRounding.Lines" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Lines" Name="linesPage" > <StackPanel Width="150" Margin="7" Orientation="Horizontal"> <!-- Single pixel line with layout rounding turned OFF.--> <Rectangle UseLayoutRounding="False" Width="45.5" Margin="10" Height="1" Fill="Red"/> <!-- Single pixel line with layout rounding turned ON.--> <Rectangle UseLayoutRounding="True" Width="45.5" Margin="10" Height="1" Fill="Red"/> </StackPanel> <!-- Background Grid --> <Page.Background> <DrawingBrush Viewport="0,0,10,10" ViewportUnits="Absolute" TileMode="Tile"> <DrawingBrush.Drawing> <DrawingGroup> <GeometryDrawing Brush="White"> <GeometryDrawing.Geometry> <RectangleGeometry Rect="0,0,1,1" /> </GeometryDrawing.Geometry> </GeometryDrawing> <GeometryDrawing Geometry="M0,0 L1,0 1,0.1, 0,0.1Z " Brush="#CCCCFF" /> <GeometryDrawing Geometry="M0,0 L0,1 0.1,1, 0.1,0Z" Brush="#CCCCFF" /> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Page.Background> </Page>
برای توضیحات بیشتر در مورد Layout Rounding میتوانید به لینک درج شده مراجعه کنید.
- Cached Composition :
مثال زیر چگونگی استفاده مجدد از عنصر cache شده را نمایش میدهد. عنصر cache شده یک Image control میباشد که یک عکس بزرگ را نمایش میدهد و کنترل تصویر به صورت bitmap با استفاده از کلاس (BitmapCache) cache شده است و از کلاس BitmapCacheBrush برای استفاده مجدد از یک عنصر ( UIElement) cache شده، استفاده شدهاست و قلم مو یا Brush اختصاص داده شده به Background از طریق بیست و پنج دکمه تا تاثیرات استفاده مجدد را نمایش دهد.
<Window x:Class="BitmapCacheBrushDemo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Window1" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="300" Width="300" > <Window.Resources> <RichTextBox x:Key="cachedRichTextBox" > <RichTextBox.CacheMode> <BitmapCache EnableClearType="True" RenderAtScale="1" SnapsToDevicePixels="True" /> </RichTextBox.CacheMode> </RichTextBox> <BitmapCacheBrush x:Key="cachedRichTextBoxBrush" Target="{StaticResource cachedRichTextBox}"> <BitmapCacheBrush.BitmapCache> <BitmapCache EnableClearType="False" RenderAtScale="0.4" SnapsToDevicePixels="False" /> </BitmapCacheBrush.BitmapCache> </BitmapCacheBrush> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button2" Grid.Column="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button3" Grid.Column="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button4" Grid.Column="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button5" Grid.Column="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button6" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button7" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button8" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button9" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button10" Grid.Row="1" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button11" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button12" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button13" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button14" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button15" Grid.Row="2" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button16" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button17" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button18" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button19" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button20" Grid.Row="3" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Name="button21" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="1" Name="button22" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="2" Name="button23" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="3" Name="button24" Grid.Row="4" FontWeight="Bold" /> <Button Background="{StaticResource cachedRichTextBoxBrush}" Content="Button" Grid.Column="4" Name="button25" Grid.Row="4" FontWeight="Bold" /> </Grid> </Window>
- Pixel Shader 3 Support :
WPF 4 از ShaderEffect که در WPF 3.5 SP1 معرفی شد پشتیبانی میکند و اجازه میهد برنامهها Effect را با استفاده از Pixel Shader(PS) version 3.0 اعمال کنند. مدل PS 3.0 Shader به طور چشمگیری پیشرفتهتر از از PS 2.0 میباشد و این باعث میشود Effectهای متنوعتر، بهتر و یا قانع کنندهتری را سخت افزارها پشتیبانی کنند.
برای آشنایی بیشتر با کلاس ShaderEffect میتوانید به لینک درج شده مراجعه نمایید.
- Easing Functions :
عنوان بخش دوم Graphics and Rendering میباشد که در ادامه آشنا خواهیم شد.
درگیر شدن با سایتهای دیگر که چرا مطالب ما را کپی کردهاید نهایتا بجز فرسایش عصبی حاصل دیگری را به همراه ندارد. اساسا زمانیکه مطلبی را به صورت باز در اینترنت انتشار میدهید، قید کپی شدن یا نشدن آنرا باید زد. اما ... میتوان همین سایتها را تبدیل به تبلیغ کنندههای رایگان کار خود نمود که در ادامه نحوه انجام آنرا در یک برنامه ASP.NET MVC بررسی خواهیم کرد:
الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.
در اینجا یک کنترلر را مشاهده میکنید که در اکشن متد Image آن، نام یک فایل دریافت شده و سپس این نام در پوشه App_Data/Images جستجو گردیده و نهایتا در مرورگر کاربر Flush میشود. از آنجائیکه الزامی ندارد fileName، واقعا یک fileName صحیح باشد، نیاز است توسط متد استاندارد Path.GetFileName این نام دریافتی اندکی تمیز شده و سپس مورد استفاده قرار گیرد. همچنین جهت کاهش بار سرور، از یک OutputCache به مدت یک روز نیز استفاده گردیده است.
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟
در ادامه توسط خاصیت سفارشی isEmbeddedIntoAnotherDomain درخواهیم یافت که درخواست رسیده، از دومین جاری صادر شده است یا خیر. اینکار توسط بررسی UrlReferrer ارسال شده توسط مرورگر صورت میگیرد. اگر Host این UrlReferrer با Host درخواست جاری یکی بود، یعنی تصویر از سایت خودمان فراخوانی شدهاست.
ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر
کلاسی در فضای نام System.Web.Helpers وجود دارد به نام WebImage که کار افزودن Watermark را بسیار ساده کرده است. نمونهای از نحوه استفاده از آنرا در متد فوق ملاحظه میکنید.
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
مشکل از اینجا است که تصاویر با فرمت ذیل برای انجام کار Watermark پشتیبانی نمیشوند:
اما میتوان تصویر دریافتی را ابتدا تبدیل به BMP کرد و سپس Watermark دار نمود:
در اینجا نمونه اصلاح شده متد addWaterMark فوق را بر اساس کار با تصاویر bmp و سپس تبدیل آنها به png، ملاحظه میکنید. به این ترتیب دیگر به خطای یاد شده بر نخواهیم خورد.
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
در اینجا اگر تشخیص داده شود که تصویر، در دومین دیگری لینک شده است، آدرس سایت ما به صورت خودکار در بالای تصویر درج خواهد شد.
کدهای نهایی این کنترلر را از اینجا میتوانید دریافت کنید:
HomeController.cs
به همراه نمونه تصویری که استثنای یاد شده را تولید میکند؛ جهت آزمایش بیشتر:
EFStra08.gif
الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.
using System.IO; using System.Net.Mime; using System.Web.Mvc; namespace MvcWatermark.Controllers { public class HomeController : Controller { const int ADay = 86400; public ActionResult Index() { return View(); } [OutputCache(VaryByParam = "fileName", Duration = ADay)] public ActionResult Image(string fileName) { fileName = Path.GetFileName(fileName); // تمیز سازی امنیتی است var rootPath = Server.MapPath("~/App_Data/Images"); var path = Path.Combine(rootPath, fileName); if (!System.IO.File.Exists(path)) { var notFoundImage = "notFound.png"; path = Path.Combine(rootPath, notFoundImage); return File(path, MediaTypeNames.Image.Gif, notFoundImage); } return File(path, MediaTypeNames.Image.Gif, fileName); } } }
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
<img src="@Url.Action(actionName: "Image", controllerName: "Home", routeValues: new { fileName = "EF_Stra_08.gif" })" />
ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟
private bool isEmbeddedIntoAnotherDomain { get { return this.HttpContext.Request.UrlReferrer != null && !this.HttpContext.Request.Url.Host.Equals(this.HttpContext.Request.UrlReferrer.Host, StringComparison.InvariantCultureIgnoreCase); } }
ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر
private byte[] addWaterMark(string filePath, string text) { var image = new WebImage(filePath); image.AddTextWatermark(text); return image.GetBytes(); }
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
A Graphics object cannot be created from an image that has an indexed pixel format.
PixelFormatUndefined PixelFormatDontCare PixelFormat1bppIndexed PixelFormat4bppIndexed PixelFormat8bppIndexed PixelFormat16bppGrayScale PixelFormat16bppARGB1555
private byte[] addWaterMark(string filePath, string text) { using (var img = System.Drawing.Image.FromFile(filePath)) { using (var memStream = new MemoryStream()) { using (var bitmap = new Bitmap(img))//avoid gdi+ errors { bitmap.Save(memStream, ImageFormat.Png); var webImage = new WebImage(memStream); webImage.AddTextWatermark(text, verticalAlign: "Top", horizontalAlign: "Left", fontColor: "Brown"); return webImage.GetBytes(); } } } }
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
if (isEmbeddedIntoAnotherDomain) { var text = Url.Action(actionName: "Index", controllerName: "Home", routeValues: null, protocol: "http"); var content = addWaterMark(path, text); return File(content, MediaTypeNames.Image.Gif, fileName); } return File(path, MediaTypeNames.Image.Gif, fileName);
کدهای نهایی این کنترلر را از اینجا میتوانید دریافت کنید:
HomeController.cs
به همراه نمونه تصویری که استثنای یاد شده را تولید میکند؛ جهت آزمایش بیشتر:
EFStra08.gif
زمانیکه قرار است با فایلهای باینری واقع در سمت سرور کار کنیم، اگر اکشن متدهای ارائه دهندهی آنها محافظت شده نباشند، برای نمایش و یا دریافت آنها تنها کافی است از آدرس مستقیم این منابع استفاده کرد و در این حالت نیازی به رعایت هیچ نکتهی خاصی نیست. اما اگر اکشن متدی در سمت سرور توسط فیلتر Authorize محافظت شده باشد و روش محافظت نیز مبتنی بر کوکیها نباشد، یعنی این کوکیها در طی درخواستهای مختلف، به صورت خودکار توسط مرورگر به سمت سرور ارسال نشوند، آنگاه نیاز است با استفاده از HttpClient برنامههای Blazor WASM، درخواست دسترسی به منبعی را به همراه برای مثال JSON Web Tokens کاربر به سمت سرور ارسال کرد و سپس فایل باینری نهایی را به صورت آرایهای از بایتها دریافت نمود. در این حالت با توجه به ماهیت Ajax ای این این عملیات، برای نمایش و یا دریافت این فایلهای محافظت شده در مرورگر، نیاز به دانستن نکات ویژهای است که در این مطلب به آنها خواهیم پرداخت.
کدهای سمت سرور دریافت فایل PDF
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامههای Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آنرا در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه میکنیم:
توضیحات:
- زمانیکه در برنامههای Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایهای از بایتهای یک فایل باینری را دریافت میکنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشتهی base64 شده صورت میگیرد (JS Interop اینکار را به صورت خودکار انجام میدهد). به همین جهت در متد createBlobUrl روش تبدیل این رشتهی base64 دریافتی را به آرایهای از بایتها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده میکنید. این Blob Url اکنون آدرس موقتی دسترسی به آرایهای از بایتهای دریافتی توسط مرورگر است. به همین جهت میتوان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آنرا انجام میدهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آنرا دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگیهای آن و سپس کلیک بر روی آنرا به صورت خودکار انجام میدهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین میکند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شدهاست، Url یک منبع را دریافت کرده، آنرا به یک iframe اضافه میکند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحهی پیشنمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین میکند.
تهیهی متدهایی الحاقی جهت کار سادهتر با JsBinaryFilesUtils
پس از تهیهی JsBinaryFilesUtils فوق، میتوان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایلهای باینری را سادهتر کرد و از تکرار کدها جلوگیری نمود:
اصلاح Content Security Policy سمت سرور جهت ارائهی محتوای blob
پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL میتوان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرسهایی به صورت blob:http تولید میشوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
در غیراینصورت مرورگر، نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و نمایش آن در صفحهی جاری
در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه میکنید:
توضیحات:
- پس از تهیهی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شدهی توسط یک اکشن متد را به صورت آرایهای از بایتها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایهای از بایتهای دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
کدهای سمت سرور دریافت فایل PDF
در اینجا کدهای سمت سرور برنامه، نکتهی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت میدهد:
using Microsoft.AspNetCore.Mvc; namespace BlazorWasmShowBinaryFiles.Server.Controllers { [ApiController] [Route("api/[controller]")] public class ReportsController : ControllerBase { [HttpGet("[action]")] public IActionResult GetPdfReport() { //TODO: create the `sample.pdf` report file on the server return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
یک نکته: استفاده مستقیم از کتابخانههای تولید PDF در برنامههای سمت کاربر Blazor منطقی نیست؛ چون به ازای هر کاربر، گاهی از اوقات مجبور به ارسال بیش از 8 مگابایت اضافی مختص به فایلهای dll. آن کتابخانهی تولید PDF خواهیم شد. بنابراین بهتر است تولید PDF را در سمت سرور و در اکشن متدهای Web API انجام داد و سپس فایل نهایی تولیدی را در برنامهی سمت کلاینت، دریافت و یا نمایش داد. به همین جهت در این مثال خروجی نهایی یک چنین عملیات فرضی را توسط یک اکشن متد Web API ارائه دادهایم که در بسیاری از موارد حتی میتواند توسط فیلتر Authorize نیز محافظت شده باشد.
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامههای Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آنرا در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه میکنیم:
window.JsBinaryFilesUtils = { createBlobUrl: function (byteArray, contentType) { // The byte array in .NET is encoded to base64 string when it passes to JavaScript. const numArray = atob(byteArray) .split("") .map((c) => c.charCodeAt(0)); const uint8Array = new Uint8Array(numArray); const blob = new Blob([uint8Array], { type: contentType }); return URL.createObjectURL(blob); }, downloadFromUrl: function (fileName, url) { const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = url; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); }, downloadBlazorByteArray: function (fileName, byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.downloadFromUrl(fileName, blobUrl); URL.revokeObjectURL(blobUrl); }, printFromUrl: function (url) { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = url; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } }, printBlazorByteArray: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.printFromUrl(blobUrl); URL.revokeObjectURL(blobUrl); }, showUrlInNewTab: function (url) { window.open(url); }, showBlazorByteArrayInNewTab: function (byteArray, contentType) { const blobUrl = this.createBlobUrl(byteArray, contentType); this.showUrlInNewTab(blobUrl); URL.revokeObjectURL(blobUrl); }, };
- زمانیکه در برنامههای Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایهای از بایتهای یک فایل باینری را دریافت میکنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشتهی base64 شده صورت میگیرد (JS Interop اینکار را به صورت خودکار انجام میدهد). به همین جهت در متد createBlobUrl روش تبدیل این رشتهی base64 دریافتی را به آرایهای از بایتها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده میکنید. این Blob Url اکنون آدرس موقتی دسترسی به آرایهای از بایتهای دریافتی توسط مرورگر است. به همین جهت میتوان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آنرا انجام میدهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آنرا دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگیهای آن و سپس کلیک بر روی آنرا به صورت خودکار انجام میدهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین میکند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شدهاست، Url یک منبع را دریافت کرده، آنرا به یک iframe اضافه میکند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحهی پیشنمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام میدهد؛ با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین میکند.
تهیهی متدهایی الحاقی جهت کار سادهتر با JsBinaryFilesUtils
پس از تهیهی JsBinaryFilesUtils فوق، میتوان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایلهای باینری را سادهتر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorWasmShowBinaryFiles.Client.Utils { public static class JsBinaryFilesUtils { public static ValueTask<string> CreateBlobUrlAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType); } public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url); } public static ValueTask DownloadBlazorByteArrayAsync( this IJSRuntime JSRuntime, string fileName, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray", fileName, byteArray, contentType); } public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url); } public static ValueTask PrintBlazorByteArrayAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType); } public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url); } public static ValueTask ShowBlazorByteArrayInNewTabAsync( this IJSRuntime JSRuntime, byte[] byteArray, string contentType) { return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType); } } }
اصلاح Content Security Policy سمت سرور جهت ارائهی محتوای blob
پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL میتوان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرسهایی به صورت blob:http تولید میشوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob: default-src 'self' blob: object-src 'self' blob:
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و نمایش آن در صفحهی جاری
در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه میکنید:
@page "/" @using BlazorWasmShowBinaryFiles.Client.Utils @inject IJSRuntime JSRuntime @inject HttpClient HttpClient <h1>Display PDF Files</h1> <button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button> <button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button> <button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button> @if(!string.IsNullOrWhiteSpace(PdfBlobUrl)) { <section class="card mb-5 mt-3"> <div class="card-header"> <h4>using iframe</h4> </div> <div class="card-body"> <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using object</h4> </div> <div class="card-body"> <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object> </div> </section> <section class="card mb-5"> <div class="card-header"> <h4>using embed</h4> </div> <div class="card-body"> <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%"> </div> </section> } @code { private const string ReportUrl = "/api/Reports/GetPdfReport"; private const string PdfContentType = "application/pdf"; private string PdfBlobUrl; private async Task handlePrintPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `PrintFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType); } private async Task handleDownloadPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `DownloadFromUrlAsync` method. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType); } private async Task handleShowPdf() { // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization). // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly. var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl); PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType); } // Tips: // 1- How do I enable/disable the built-in pdf viewer of FireFox // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709). }
- پس از تهیهی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شدهی توسط یک اکشن متد را به صورت آرایهای از بایتها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایهای از بایتهای دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
وین فرمز در دنیای WPF در حد فقط یکی از حالتهای طرح بندی آن به نام Canvas است (چندین سیستم طرح بندی دارد ...). Silverlight هم برادر کوچکتر WPF محسوب میشود. بنابراین اگر موردی را بتوان با winforms پیاده سازی کرد و با WPF و Silverlight خیر، بشنوید اما باور نکنید. صد البته Silverlight محدود است به sandbox مرورگر و دسترسی امنیتی آن در این حد است اما قابلیت اجرای خارج از مرورگر با سطح دسترسی بالاتر (پس از تائید کاربر را هم دارد). دسترسی کاملی به WCF دارد و همچنین نسخهی 4 آن قابلیت تعامل با اشیاء COM را هم دارد (این کنترل پنلهای مدیریتی تحت وب از همین دو مورد اخیر ناشی میشوند + قابلیتهای پاور شل جدید ویندوز سرور که دسترسی از راه دور را سادهتر کرده).
علت استفاده اصلی از سیلورلایت هم پایین آوردن زمان، هزینه و همچنین سادگی توسعه است. (به این موارد در مقالهای که آدرس دادم اشاره شده ...)
ضمنا تکلیف MVC مشخص است (حداقل بعد از سه نگارش آن). نسخهی بعدی Winforms هم تاجایی که عنوان شده قرار است چندین قابلیت MVC مانند Model binders و validation آنرا به ارث ببرد.
پ.ن.
بین ماکرو و میکرو تفاوت از زمین تا آسمان است.
علت استفاده اصلی از سیلورلایت هم پایین آوردن زمان، هزینه و همچنین سادگی توسعه است. (به این موارد در مقالهای که آدرس دادم اشاره شده ...)
ضمنا تکلیف MVC مشخص است (حداقل بعد از سه نگارش آن). نسخهی بعدی Winforms هم تاجایی که عنوان شده قرار است چندین قابلیت MVC مانند Model binders و validation آنرا به ارث ببرد.
پ.ن.
بین ماکرو و میکرو تفاوت از زمین تا آسمان است.
یکی از مهمترین قسمتهای برنامه، کار با دادههای بانک اطلاعاتی (یا در کل منابع اطلاعاتی) است. اینکه چگونه با آنها ارتباط برقرار کنیم و آنها را در یک قالب کاربر پسند به کاربران برنامه نشان دهیم. افزودن شیء DataContext و مفاهیمی چون DataBinding باعث ارتباط سریعتر و راحتتری با منبع دادهها شده است. همچنین این قابلیت وجود دارد که هر گونه به روز آوری در اطلاعات دریافت شده، شما را با خبر سازد تا بتوانید طبق آن چه که میخواهید اطلاعات نمایشی را به روز کنید. در این مقاله به نحوهی ارتباط بین منبع داده با DataContext و سپس کنترلهایی را چون Grid و ListBox و ... در رابطه با این منابع داده بررسی میکنیم.
در مورد بررسی ارتباط با دادهها در WPF باید سه مورد را بشناسیم:
ابتدا قبل از هر چیزی کلاس فرم قبلی را پیاده سازی میکنیم. در این پیاده سازی از یک enum برای انتخاب زمینههای کاری هم کمک گرفته ایم و هچنین با یک متد ایستا، منبع دادهی تک رکوردی را جهت تست برنامه آماده کردهایم:
در مورد بررسی ارتباط با دادهها در WPF باید سه مورد را بشناسیم:
- DataContext: این شیء اتصالش را به منبع دادهها برقرار کرده و هر موقع دادهای را نیاز داریم، از طریق این شیء تامین میشود.
- DataBinding: یک واسطه بین DataContext و هر آن چیزی است که قرار است از دادهها تغذیه کند. در تعریفی رسمیتر میگوییم: روشی ساده و قدرتمند بوده و واسطی است بین مدل تجاری و رابط کاربری. هر زمانی که دادهای تغییر کند، ما را آگاه میسازد که میتواند یک ارتباط یک طرفه یا دو طرفه باشد.
- DataTemplate: نحوهی فرمت بندی و نمایش دادهها را تعیین میکند.
ابتدا قبل از هر چیزی کلاس فرم قبلی را پیاده سازی میکنیم. در این پیاده سازی از یک enum برای انتخاب زمینههای کاری هم کمک گرفته ایم و هچنین با یک متد ایستا، منبع دادهی تک رکوردی را جهت تست برنامه آماده کردهایم:
public enum FieldOfWork { Actor=0, Director=1, Producer=2 } public class Person { public string Name { get; set; } public bool Gender { get; set; } public string ImageName { get; set; } public string Country { get; set; } public DateTime Date { get; set; } public IList<FieldOfWork> FieldOfWork { get; set; } public static Person GetPerson() { return new Person() { Name = "Leo", Gender = true, ImageName ="man.jpg", Country = "Italy", Date = DateTime.Now }; } }
حالا لازم است که این منبع داده را در اختیار DataContext بگذاریم. وارد بخش کد نویسی شده و در سازندهی پنجره کد زیر را مینویسیم:
DataContext = Person.GetPerson();
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = Person.GetPerson(); } }
با این کار، ارتباط شما با منبع داده آغاز میشود و طبق درخواستهایی که از DataBinding به آن میرسد، اطلاعات را تحویل DataBinding میدهد. برای نمایش دادهها در کنترلها و استفاده از DataBinding، به سراغ خصوصیات وابسته میرویم. در حال حاضر فعلا برنامه را با دو کنترل عکس و نام که رشتهای هستند آغاز میکنیم؛ چون بقیهی کنترلها کمی متفاوت هستند.
همانطور که میدانید متن کنترل TextBox توسط خصوصیت Text پر میشود و برای همین در این خصوصیت مینویسیم:
همانطور که میدانید متن کنترل TextBox توسط خصوصیت Text پر میشود و برای همین در این خصوصیت مینویسیم:
Text="{Binding Name}"
Source="{Binding ImageName}"
کلمهی Leo داخل کادر متنی قرار گرفته و عکس اینبار به صورت ایستا خوانده نشده، بلکه نام عکس از طریق یک منبع داده برای آن فراهم شده است.
اطلاع از به روزرسانی در منبع دادهها:
حال این نکته پیش میآید که اگر همین اطلاعات دریافت شده در مدل منبع داده تغییر کند، چگونه میتوانیم از این موضوع مطلع شده و همین اطلاعات به روز شده را که نمایش دادهایم، تغییر دهیم. بنابراین جهت اطلاع از این مورد، کد را به شکل زیر تغییر میدهیم.
کار را از یک کلاس آغاز میکنیم. از اینترفیس INotifyPropertyChanged ارث بری کرده و در آن یک رویداد و یک متد را تعریف میکنیم و کمی در هم در تعریف Propertyها دست میبریم. فعلا اینکار را فقط برای پراپرتی Name انجام میدهیم:
اطلاع از به روزرسانی در منبع دادهها:
حال این نکته پیش میآید که اگر همین اطلاعات دریافت شده در مدل منبع داده تغییر کند، چگونه میتوانیم از این موضوع مطلع شده و همین اطلاعات به روز شده را که نمایش دادهایم، تغییر دهیم. بنابراین جهت اطلاع از این مورد، کد را به شکل زیر تغییر میدهیم.
کار را از یک کلاس آغاز میکنیم. از اینترفیس INotifyPropertyChanged ارث بری کرده و در آن یک رویداد و یک متد را تعریف میکنیم و کمی در هم در تعریف Propertyها دست میبریم. فعلا اینکار را فقط برای پراپرتی Name انجام میدهیم:
private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged("Name"); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } }
در کد بالا یک رویداد از نوع PropertyChangedEventHandler تعریف میکنیم که وظیفهی به روزآوری را به عهده دارد؛ ولی صدا زدن این رویداد بر عهدهی ماست و خود به خود صدا زده نمیشود. پس نیاز است متدی را فراهم کرده تا بدانیم که چه خصوصیتی تغییر یافتهاست و از آن طریق رویداد را فراخوانی کنیم و به رویداد بگوییم که کدام پراپرتی تغییر کرده است. این متد را OnpropertyChanged مینامیم که آرگومان ورودی آن نام خصوصیتی است که تغییر یافته است و پس از ارزیابی از صحت آن، رویداد را Invoke میکنیم.
در بخش Setter آن خصوصیت هم باید این متد را صدا زده و نام خصوصیت را به آن پاس بدهیم تا موقعی که مدل تغییر پیدا کرد، بگوید که خصوصیت Name بوده است که تغییر کرده است.
برای اینکه بدانیم کد واقعا کار میکند و تستی بر آن زده باشیم، فعلا دکمهی Save را به Change تغییر میدهیم و کد داخل پنجره را بدین صورت تغییر میدهیم:
در بخش Setter آن خصوصیت هم باید این متد را صدا زده و نام خصوصیت را به آن پاس بدهیم تا موقعی که مدل تغییر پیدا کرد، بگوید که خصوصیت Name بوده است که تغییر کرده است.
برای اینکه بدانیم کد واقعا کار میکند و تستی بر آن زده باشیم، فعلا دکمهی Save را به Change تغییر میدهیم و کد داخل پنجره را بدین صورت تغییر میدهیم:
public partial class MainWindow : Window { private Person person; public MainWindow() { InitializeComponent(); person = Person.GetPerson(); DataContext = person; } private void Button_Click(object sender, RoutedEventArgs e) { person.Name = "Leonardo Decaperio"; } }
متغیر کلاسی را از حالت محلی Local به عمومی Global تغییر دادم که از طریق دکمهی منبع داده در دسترس باشد. حال در رویداد دکمه نام بازیگر را تغییر میدهم. برنامه را اجرا کنید و بر روی دکمه کلیک کنید. باید بعد از یک لحظهی کوتاه، نام بازیگر از Leo به Leonardo Decaperio تغییر کند.
این کد واقعا کدی مفید جهت به روزرسانی است ولی مشکلی دارد که نام پراپرتی باید به صورت String به آن پاس شود که در یک برنامه بزرگ این مورد یک مشکل خواهد شد و اگر نام خصوصیت تغییر کند باید نام داخل آن هم تغییر کند؛ پس کد را به شکل دیگری بازنویسی میکنیم:
این کد واقعا کدی مفید جهت به روزرسانی است ولی مشکلی دارد که نام پراپرتی باید به صورت String به آن پاس شود که در یک برنامه بزرگ این مورد یک مشکل خواهد شد و اگر نام خصوصیت تغییر کند باید نام داخل آن هم تغییر کند؛ پس کد را به شکل دیگری بازنویسی میکنیم:
private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } private void OnPropertyChanged([CallerMemberName] string property="") { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } }
در متد OnPropertyChanged در کنار پارامتر اول، ویژگی attribute به نام CallerMemberName را که در فضای نام system.runtime.compilerservice قرار دارد استفاده میکنیم (دات نت 4.5). این ویژگی، نام پراپرتی یا متدی که متد OnpropertyChnaged را صدا زده است، به دست میآورد. پارامتر اول را هم اختیاری میکنیم که سیستم بر ورود پارامتر اجباری نداشته باشد و نهایتا در هر پراپرتی تنها لازم است همانند بالا، خط زیر ذکر شود:
OnPropertyChanged();
کد این قسمت
در قسمتهای آینده به بررسی تبدیل مقادیر و framework element و کنترلها میپردازیم.
پاسخ به بازخوردهای پروژهها
کلیک بر روی لینک ها
برای کار کرد درست، نیاز به قطع اینترنت دارد چون تحت کنترل قرار دادن
لینکها در کنترل وب WPF نیاز به بارگذاری کامل صفحه دارد و در حالت اتصال
به اینترنت این بارگذاری دیرتر صورت میگیرد؛ ولی در حالت قطع از اینترنت، آنی
است.
در قالب طراحی شده، نه در کدهای Viewهای اضافه شده و نه در ViewModelها، اثری از کدهای مرتبط با تزریق وابستگیها و یا حتی وهله سازی ViewModel مرتبط با یک View مشاهده نمیشود. در ادامه قصد داریم جزئیات پیاده سازی آنرا مرور کنیم.
مدیریت خودکار وهله سازی ViewModelها
اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام میدهد، مشاهده خواهید کرد:
در این کلاس، یک وهله از صفحهای که توسط کاربر درخواست شدهاست، در سازنده کلاس دریافت گردیده و سپس در متد WireUp، بر اساس قرارداد نامگذاری که پیشتر نیز عنوان شد، ViewModel متناظر با نام View از IoC Container استخراج و وهله سازی میگردد. سپس این وهله به DataContext صفحه انتساب داده میشود.
چند سؤال مهم:
- IoC Container از کجا میداند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهلهای از یک صفحه درخواستی توسط کاربر دسترسی پیدا میکند و در کجا؟
IoC Container از کجا میداند که ViewModelها در کجا قرار دارند؟
اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایهای به نام BaseViewModel تهیه کنیم. برای مثال:
این کلاس پایه که در فایل MVVM\BaseViewModel.cs پروژه Common قرار دارد، به نحو زیر آغاز شده است:
اگر دقت کنید در اینجا اینترفیس IViewModel نیز ذکر شده است. این اینترفیس برای علامتگذاری ViewModelها و یافتن خودکار آنها توسط IoC Container مورد استفاده درنظر گرفته شده است. اگر به فایل Core\IocConfig.cs پروژه Infrastructure مراجعه کنید، چنین تنظیمی را در آن مشاهده خواهید نمود:
به این ترتیب StructureMap با اسکن اسمبلی Infrastructure کلیه کلاسهای پیاده سازی کننده IViewModel را یافته و سپس آنها را بر اساس نام متناظری که دارند، ذخیره میکند. با این تنظیم، اکنون در کلاس ViewModelFactory یک چنین کدی کار خواهد کرد:
کلاس ViewModelFactory چگونه به وهلهای از یک صفحه درخواستی توسط کاربر دسترسی پیدا میکند و در کجا؟
در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه میکنید:
در این کلاس، یک Frame سفارشی را طراحی کردهایم؛ از این جهت که بتوان متد OnContentChanged آنرا تحریف کرد. در این متد، newContent دقیقا وهلهای از صفحه جدیدی است که توسط کاربر درخواست شدهاست. خوب ... این وهله را داریم، بنابراین تنها کافی است آنرا به کلاس ViewModelFactory ارسال کنیم و متد WireUp آنرا بر روی وهله کلاس صفحه درخواستی فراخوانی نمائیم. به این ترتیب، صفحهای نمایش داده خواهد شد که DataContext آن با وهلهای از ViewModel متناظر مقدار دهی شدهاست. از این جهت که این وهله سازی توسط IoC Container صورت میگیرد، کلیه وابستگیهای تعریف شده در سازنده کلاس ViewModel نیز به صورت خودکار وهله سازی و مقدار دهی خواهند شد.
نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.
مدیریت خودکار وهله سازی ViewModelها
اگر به فایل MVVM\ViewModelFactory.cs قرار گرفته در پروژه Common مراجعه کنید، کدهای کلاسی که کار وهله سازی ViewModelها را انجام میدهد، مشاهده خواهید کرد:
using System.Windows; using StructureMap; namespace WpfFramework1999.Common.MVVM { /// <summary> /// Stitches together a view and its view-model /// </summary> public class ViewModelFactory { private readonly FrameworkElement _control; /// <summary> /// سازنده کلاس تزریق وابستگیها به ویوو مدل و وهله سازی آن /// </summary> /// <param name="control">وهلهای از شیءایی که باید کار تزریق وابستگیها در آن انجام شود</param> public ViewModelFactory(FrameworkElement control) { _control = control; } /// <summary> /// وهله متناظر با ویوو مدل /// </summary> public IViewModel ViewModelInstance { get; private set; } /// <summary> /// کار تزریق خودکار وابستگیها و وهله سازی ویوو مدل مرتبط انجام خواهد شد /// </summary> public void WireUp() { var viewName = _control.GetType().Name; var viewModelName = string.Concat(viewName, "ViewModel"); //قرار داد نامگذاری ما است if (!_control.IsLoaded) { _control.Loaded += (s, e) => { setDataContext(viewModelName); }; } else { setDataContext(viewModelName); } } private void setDataContext(string viewModelName) { //کار تزریق خودکار وابستگیها و وهله سازی ویوو مدل مرتبط انجام خواهد شد ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName); if (ViewModelInstance == null) // این صفحه ویوو مدل ندارد return; _control.DataContext = ViewModelInstance; } } }
چند سؤال مهم:
- IoC Container از کجا میداند که ViewModelها در کجا قرار دارند؟
- این کلاس ViewModelFactory چگونه به وهلهای از یک صفحه درخواستی توسط کاربر دسترسی پیدا میکند و در کجا؟
IoC Container از کجا میداند که ViewModelها در کجا قرار دارند؟
اگر بحث سری جاری را از ابتدا دنبال کرده باشید، عنوان شد که ViewModelها را در این قالب، باید مشتق شده از کلاس پایهای به نام BaseViewModel تهیه کنیم. برای مثال:
/// <summary> /// ویوو مدل افزودن و مدیریت کاربران /// </summary> public class AddNewUserViewModel : BaseViewModel
/// <summary> /// کلاس پایه ویوو مدلهای برنامه که جهت علامتگذاری آنها برای سیم کشیهای تزریق وابستگیهای برنامه نیز استفاده میشود /// </summary> public abstract class BaseViewModel : DataErrorInfoBase, INotifyPropertyChanged, IViewModel
// Add all types that implement IView into the container, // and name each specific type by the short type name. scan.AddAllTypesOf<IViewModel>().NameBy(type => type.Name);
//کار تزریق خودکار وابستگیها و وهله سازی ویوو مدل مرتبط انجام خواهد شد ViewModelInstance = ObjectFactory.TryGetInstance<IViewModel>(viewModelName);
کلاس ViewModelFactory چگونه به وهلهای از یک صفحه درخواستی توسط کاربر دسترسی پیدا میکند و در کجا؟
در اینجا قسمتی از کدهای فایل Core\FrameFactory.cs قرار گرفته در پروژه Infrastructure را ملاحظه میکنید:
namespace WpfFramework.Infrastructure.Core { /// <summary> /// ایجاد یک کنترل فریم سفارشی که قابلیت تزریق وابستگیها را به صورت خودکار دارد /// به همراه اعمال مسایل راهبری برنامه که از منوی اصلی دریافت میشوند /// </summary> public class FrameFactory : Frame { /// <summary> /// در اینجا میشود به وهلهای از صفحهای که قرار است اضافه گردد دسترسی یافت /// </summary> protected override void OnContentChanged(object oldContent, object newContent) { base.OnContentChanged(oldContent, newContent); var newPage = newContent as FrameworkElement; if (newPage == null) return; _currentViewModelFactory = new ViewModelFactory(newPage); _currentViewModelFactory.WireUp(); //کار تزریق وابستگیها و وهله سازی ویوو مدل مرتبط انجام خواهد شد } } }
نهایتا فراخوانی متد IocConfig.Init، در فایل App.xaml.cs پروژه ریشه، در آغاز برنامه قرار گرفته است.