- دارای مجوز MIT است. (مجاز هستید از آن در هر نوع برنامهای استفاده کنید)
- cross-platform است. به این معنا که دات نت، WinRT و Xamarin را به خوبی پشتیبانی میکند.
- WPF و همچنین WinForms تا Xamarin.Android را پوشش میدهد.
- بستههای اصلی NuGet آن تا به امروز نزدیک به 40 هزار بار دریافت شدهاند.
- انجمن فعالی دارد.
- بسیار بسیار غنی است. تا حدی که مرور سطحی مجموعه مثالهای آن شاید چند ساعت وقت را به خود اختصاص دهد.
- طراحی آن به نحوی است که با الگوی MVVM کاملا سازگاری دارد.
- به صورت متناوبی به روز شده و نگهداری میشود.
این برنامه (تصویر فوق) که حاوی مرورگر مثالهای آن است، در پوشهی Source\Examples\WPF\ExampleBrowser سورسهای آن قرار دارد.
در ادامه نگاهی خواهیم داشت به نحوهی استفاده از OxyPlot در برنامههای WPF جهت رسم نموداری بلادرنگ که اطلاعات آن در زمان اجرای برنامه تهیه شده و در همین حین نیز تغییر میکنند.
دریافت بستههای نیوگت OxyPlot
برای دریافت دو بستهی OxyPlot.Core و OxyPlot.Wpf تنها کافی است دستور ذیل را در کنسول پاورشل نیوگت اجرا کنیم:
PM> install-package OxyPlot.Wpf
افزودن تعاریف چارت به View
<Window x:Class="OxyPlotWpfTests.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:oxy="http://oxyplot.org/wpf" xmlns:oxyPlotWpfTests="clr-namespace:OxyPlotWpfTests" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <oxyPlotWpfTests:MainWindowViewModel x:Key="MainWindowViewModel" /> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource MainWindowViewModel}}"> <oxy:PlotView Model="{Binding PlotModel}"/> </Grid> </Window>
ساختار کلی ViewModel برنامه
کار ViewModel متصل شده به View فوق، مقدار دهی PlotModel است.
public class MainWindowViewModel { public PlotModel PlotModel { get; set; }
یک نکتهی کاربردی
اگر هیچ ایدهای نداشتید که این PlotModel را چگونه باید مقدار دهی کرد، به همان برنامهی ExampleBrowser ابتدای مطلب مراجعه کنید.
مثالهای اجرای شدهی آن یک برگهی نمایشی و یک برگهی Code دارند. خروجی این متدها را اگر به خاصیت PlotModel فوق انتساب دهید ... یک چارت کامل خواهید داشت.
مراحل ساخت یک PlotModel
ابتدا نیاز است یک وهلهی جدید از PlotModel را ایجاد کنیم:
private void createPlotModel() { PlotModel = new PlotModel { Title = "سری خطوط", Subtitle = "Pan (right click and drag)/Zoom (Middle click and drag)/Reset (double-click)" }; PlotModel.MouseDown += (sender, args) => { if (args.ChangedButton == OxyMouseButton.Left && args.ClickCount == 2) { foreach (var axis in PlotModel.Axes) axis.Reset(); PlotModel.InvalidatePlot(false); } }; }
برای pan، کافی است دکمهی سمت راست ماوس را نگه داشته و بکشید. به این ترتیب میتوانید نمودار را بر روی محورهای X و Y حرکت دهید.
برای zoom نیاز است دکمهی وسط ماوس را نگه داشته و بکشید. ناحیهای که در این حالت نمایان میگردد، محل بزرگنمایی نهایی خواهد بود.
این دو قابلیت به صورت توکار در OxyPlot قرار دارند و نیازی به کدنویسی برای فعال سازی آنها نیست.
افزودن محورهای X و Y
محور X در مثال ما، از نوع LinearAxis است. بهتر است متغیر آنرا در سطح کلاس تعریف کرد تا بتوان از آن در سایر قسمتهای چارت نیز بهره گرفت:
readonly LinearAxis _xAxis = new LinearAxis(); private void addXAxis() { _xAxis.Minimum = 0; _xAxis.MaximumPadding = 1; _xAxis.MinimumPadding = 1; _xAxis.Position = AxisPosition.Bottom; _xAxis.Title = "X axis"; _xAxis.MajorGridlineStyle = LineStyle.Solid; _xAxis.MinorGridlineStyle = LineStyle.Dot; PlotModel.Axes.Add(_xAxis); }
مقدار دهی GridlineStyleها سبب ایجاد یک Grid خاکستری در نمودار میشوند.
در آخر نیاز است این محور به محورهای PlotModel اضافه شود.
تعریف محور Y نیز به همین نحو است. اگر مقدار خاصیت Position ذکر نشود، این محور در سمت چپ صفحه قرار میگیرد:
readonly LinearAxis _yAxis = new LinearAxis(); private void addYAxis() { _yAxis.Minimum = 0; _yAxis.Title = "Y axis"; _yAxis.MaximumPadding = 1; _yAxis.MinimumPadding = 1; _yAxis.MajorGridlineStyle = LineStyle.Solid; _yAxis.MinorGridlineStyle = LineStyle.Dot; PlotModel.Axes.Add(_yAxis); }
افزودن تعاریف سریهای خطوط
در تصویر فوق، دو سری خط را ملاحظه میکنید. تعاریف پایه سری اول آن به این صورت است:
readonly LineSeries _lineSeries1 = new LineSeries(); private void addLineSeries1() { _lineSeries1.MarkerType = MarkerType.Circle; _lineSeries1.StrokeThickness = 2; _lineSeries1.MarkerSize = 3; _lineSeries1.Title = "Start"; _lineSeries1.MouseDown += (s, e) => { if (e.ChangedButton == OxyMouseButton.Left) { PlotModel.Subtitle = "Index of nearest point in LineSeries: " + Math.Round(e.HitTestResult.Index); PlotModel.InvalidatePlot(false); } }; PlotModel.Series.Add(_lineSeries1); }
هر سری دارای خاصیت MouseDown نیز هست. برای مثال اگر علاقمندید که کلیک کاربر بر روی نقاط مختلف را دریافت کرده و سپس بر این اساس، اطلاعات خاصی را نمایش دهید، میتوانید از مقدار e.HitTestResult.Index استفاده کنید. در اینجا ایندکس نزدیکترین نقطه به محل کلیک کاربر یافت میشود.
تعاریف اولیه سری دوم نیز به همین ترتیب هستند:
readonly LineSeries _lineSeries2 = new LineSeries(); private void addLineSeries2() { _lineSeries2.MarkerType = MarkerType.Circle; _lineSeries2.Title = "End"; _lineSeries2.StrokeThickness = 2; _lineSeries2.MarkerSize = 3; _lineSeries2.MouseDown += (s, e) => { if (e.ChangedButton == OxyMouseButton.Left) { PlotModel.Subtitle = "Index of nearest point in LineSeries: " + Math.Round(e.HitTestResult.Index); PlotModel.InvalidatePlot(false); } }; PlotModel.Series.Add(_lineSeries2); }
به روز رسانی دستی OxyPlot
پس از نمایش اولیه OxyPlot، هر تغییری که در اطلاعات آن صورت گیرد، نمایش داده نخواهد شد. برای به روز رسانی آن فقط کافی است متد PlotModel.InvalidatePlot را فراخوانی نمائید. برای نمونه در متدهای فوق، کلیک ماوس، پس از رسم نمودار انجام میشود. بنابراین اگر نیاز است زیرعنوان نمودار تغییر کند، باید متد PlotModel.InvalidatePlot نیز فراخوانی گردد.
ایجاد یک تایمر برای افزودن نقاط به صورت پویا
در ادامه میخواهیم نقاطی را به صورت پویا به نمودار اضافه کنیم. نمایش یکباره نمودار، نکتهی خاصی ندارد. تنها کافی است توسط lineSeries1.Points.Add یک سری DataPoint را اضافه کنید. این نقاط در زمان نمایش View، به یکباره نمایش داده خواهند شد. اما در اینجا ابتدا یک چارت خالی نمایش داده میشود و سپس قرار است نقاطی به آن اضافه شوند.
private int _xMax; private int _yMax; private bool _haveNewPoints; private void addPoints() { var timer = new DispatcherTimer {Interval = TimeSpan.FromSeconds(1)}; var rnd = new Random(); var x = 1; updateXMax(x); timer.Tick += (sender, args) => { var y1 = rnd.Next(100); updateYMax(y1); _lineSeries1.Points.Add(new DataPoint(x, y1)); var y2 = rnd.Next(100); updateYMax(y2); _lineSeries2.Points.Add(new DataPoint(x, rnd.Next(y2))); x++; updateXMax(x); _haveNewPoints = true; }; timer.Start(); } private void updateXMax(int value) { if (value > _xMax) { _xMax = value; } } private void updateYMax(int value) { if (value > _yMax) { _yMax = value; } }
- افزودن نقاط جدید توسط متدهای lineSeries1.Points.Add انجام میشوند.
- مقادیر max محورهای x و y را نیز ذخیره میکنیم. اگر نقاط برنامه پویا نباشند، OxyPlot به صورت خودکار نمودار را با مقیاس درستی ترسیم میکند. اما اگر نقاط پویا باشند، نیاز است حداکثر محورهای x و y را به صورت دستی در آن تنظیم کنیم. به همین جهت متدهای updateXMax و updateYMax در اینجا فراخوانی شدهاند.
- به روز رسانی ظاهر چارت، توسط متد زیر انجام میشود:
private readonly Stopwatch _stopwatch = new Stopwatch(); private void updatePlot() { CompositionTarget.Rendering += (sender, args) => { if (_stopwatch.ElapsedMilliseconds > _lastUpdateMilliseconds + 2000 && _haveNewPoints) { if (_yMax > 0 && _xMax > 0) { _yAxis.Maximum = _yMax + 3; _xAxis.Maximum = _xMax + 1; } PlotModel.InvalidatePlot(false); _haveNewPoints = false; _lastUpdateMilliseconds = _stopwatch.ElapsedMilliseconds; } }; }
نکتهی دیگری که در متد updatePlot فوق درنظر گرفته شدهاست، تغییر مقدار Maximum محورهای x و y بر اساس حداکثرهای نقاط اضافه شدهاست. به این ترتیب نمودار به صورت خودکار جهت نمایش کل اطلاعات، تغییر اندازه خواهد داد.
البته همانطور که عنوان شد، تمام این تهمیدات برای نمایش نمودارهای بلادرنگ است. اگر کار مقدار دهی Points.Add را فقط یکبار در سازندهی ViewModel انجام میدهید، نیازی به این نکات نخواهید داشت.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید:
OxyPlotWpfTests.zip
اکثر قلمهای فارسی، فاقد تعاریف مرتبط با حروف انگلیسی هستند. البته عموم کاربران متوجه این امر نمیشوند چون ویندوز دو مفهوم Font Fallback و Font Linking را جهت پوشش glyph های تعریف نشده، در پشت صحنه اعمال خواهد کرد. جزئیات بیشتر در اینجا: (^ و ^)
به صورت خلاصه کار Font Fallback در ویندوز جایگزینی خودکار قلم مورد استفاده است؛ تحت شرایط زیر:
- فونت تعریف شده در برنامه، در سیستم کاربر وجود نداشته باشد.
- تعاریف Glyphهای بکارگرفته شده در متن جاری، در قلم انتخابی وجود نداشته باشند.
در WPF این مساله کاملا قابل کنترل است. قلمی که به صورت خودکار به عنوان جایگزین مطرح میشود در قلمی به نام "Global User Interface" تعریف شده است. تعاریف این قلم ترکیبی هم در فایلی به نام GlobalUserInterface.CompositeFont در پوشه فونتهای سیستم موجود است (برای مثال، مسیر c:\windows\fonts حاوی این فایل متنی است).
اگر این فایل XML را با یک ادیتور متنی باز کنید، مشاهده خواهید کرد که بازههای مختلف کاراکترهای یونیکد، به فونتهای پیش فرضی نگاشت شدهاند. بنابراین اگر این سؤال وجود دارد که در متن مخلوط فارسی و انگلیسی من، فونت پیش فرض حروف انگلیسی از کجا تامین و مشخص میشود، پاسخ را در این فایل میتوانید مشاهده کنید.
روش دیگری هم برای تعیین Fallback font در WPF وجود دارد. یک مثال:
<Window x:Class="WpfFontTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<TextBlock
Text="نمایش مخلوطی از متن فارسی و متن English با هم"
Margin="7"
FontFamily="Fonts/BNazanin.ttf#B Nazanin, Comic Sans Ms"
FontSize="25"
FlowDirection="RightToLeft"
VerticalAlignment="Top" HorizontalAlignment="Center" />
</Grid>
</Window>
در این مثال فونت B Nazanin در برنامه قرار داده شده است (embedded font). همچنین در کنار آن پس از علامت کاما، Fallback font مشخص است. به این معنا که تاجایی که میسر است لطفا از فونت B Nazanin برای نمایش متن مورد نظر استفاده شود؛ اگر نشد از قلم Comic Sans Ms استفاده گردد. قلم B Nazanin حاوی تعاریف حروف انگلیسی نیست. بنابراین WPF جهت نمایش آنها از فونت دوم معرفی شده کمک میگیرد. توضیحات بیشتر در اینجا: (^)
استفاده از Async و Await در برنامههای دسکتاپ
تهیه مقدمات بحث
ابتدا یک برنامهی WPF جدید را آغاز کنید. سپس کدهای MainWindow.xaml آنرا به نحو ذیل تغییر دهید.
<Window x:Class="Async10.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <DockPanel> <DockPanel Dock="Top"> <Button Name="BtnGo" Content="Go" Click="BtnGo_OnClick" /> <ProgressBar Name="ProgressBar" IsIndeterminate="True" Visibility="Collapsed"/> </DockPanel> <TextBox Name="Results"/> </DockPanel> </Window>
در این مثال از کلاس جدید HttpClient نیز استفاده خواهیم کرد. برای استفاده از آن نیاز است ارجاعی را به اسمبلی استاندارد System.Net.Http.dll نیز به پروژه اضافه کنید.
روش اول
در ادامه کدهای فایل MainWindow.xaml.cs را به نحو ذیل تغییر داده و سپس برنامه را اجرا کنید.
using System.Net.Http; using System.Windows; namespace Async10 { public partial class MainWindow { public MainWindow() { InitializeComponent(); } private void BtnGo_OnClick(object sender, RoutedEventArgs e) { BtnGo.IsEnabled = false; ProgressBar.Visibility = Visibility.Visible; var url = "https://www.dntips.ir"; var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async"); var task = client.GetStringAsync(url); task.ContinueWith(t => { Results.Text = t.Result; BtnGo.IsEnabled = true; ProgressBar.Visibility = Visibility.Collapsed; }); } } }
اگر پروژه را اجرا کنید، برنامه با استثنای زیر متوقف میشود:
The calling thread cannot access this object because a different thread owns it.
الف) با استفاده از SynchronizationContext.Current و متد Post آن
var context = SynchronizationContext.Current; task.ContinueWith(t => context.Post(state => { Results.Text = t.Result; BtnGo.IsEnabled = true; ProgressBar.Visibility = Visibility.Collapsed; }, null));
ب) با استفاده از امکانات TaskScheduler
var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext(); task.ContinueWith(t => { Results.Text = t.Result; BtnGo.IsEnabled = true; ProgressBar.Visibility = Visibility.Collapsed; }, taskScheduler);
برای مثال اگر در برنامههای وب یک Task جدید را اجرا کنید شاید اینطور به نظر برسد که به HttpContext دسترسی ندارید. این نقیصه را میتوان توسط کار با SynchronizationContext جاری برطرف کرد.
در مثال فوق، چون taskScheduler پیش از فراخوانی متد ContinueWith ایجاد شدهاست، به ترد UI اشاره میکند. در این حالت برای نمایش اطلاعات در همان ترد اصلی برنامه کافی است این taskScheduler را به عنوان پارامتر متد ContinueWith معرفی کنیم.
روش دوم
در دات نت 4.5 میتوان روال رخدادگردان تعریف شده را به صورت async نیز معرفی کرد (یعنی مجاز هستیم امضای متد پیش فرض تولید شده را تغییر دهیم):
private async void BtnGo_OnClick(object sender, RoutedEventArgs e)
private async void BtnGo_OnClick(object sender, RoutedEventArgs e) { BtnGo.IsEnabled = false; ProgressBar.Visibility = Visibility.Visible; var url = "https://www.dntips.ir"; var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async"); Results.Text = await client.GetStringAsync(url); BtnGo.IsEnabled = true; ProgressBar.Visibility = Visibility.Collapsed; }
تا صحبت از گزارشگیری به میان بیاید احتمالا معرفی ابزارهای تجاری مانند Reporting services ، کریستال ریپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر لیست توصیه کنندگان و مشاوران قرار خواهند داشت. اما خوب برای ایجاد یک گزارشگیری ساده حتما نیازی نیست تا به این نوع ابزارهای تجاری مراجعه کرد. ابزار رایگان و سورس باز جالبی هم در این باره جهت پروژههای WPF در دسترس است:
در ادامه در طی یک مثال قصد داریم از این کتابخانه استفاده کنیم:
1) تنظیم وابستگیها
پس از دریافت کتابخانه فوق، ارجاعات زیر باید به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلیهای استاندارد دات نت است)
2) تهیه منبع داده گزارش
کتابخانهی فوق به صورت پیش فرض با DataTable کار میکند. بنابراین کوئریهای شما یا باید خروجی DataTable داشته باشد یا باید از یک سری extension methods برای تبدیل IEnumerable به DataTable استفاده کرد (در پروژه پیوست شده در پایان مطلب، این موارد موجود است).
برای مثال فرض کنید میخواهیم رکوردهایی را از نوع کلاس Product زیر در گزارش نمایش دهیم:
namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
الف) اضافه کردن فایل تشکیل دهنده ساختار و ظاهر گزارش
گزارشهای این کتابخانه مبتنی است بر اشیاء FlowDocument استاندارد WPF . بنابراین از منوی پروژه گزینهی Add new item در قسمت WPF آن یک FlowDocument جدید را به پروژه اضافه کنید ( باید دقت داشت که Build action این فایل باید به Content تنظیم گردد). ساختار ابتدایی این FlowDocument به صورت زیر خواهد بود که به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسی اضافه شده است. همچنین فضای نام مربوط به کتابخانهی گزارشگیری CodeReason.Reports نیز باید اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">
</FlowDocument>
مواردی که در ادامه ذکر خواهند شد محتوای این گزارش را تشکیل میدهند:
ب) مشخص سازی خواص گزارش
<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
ج) مشخص سازی Page Header و Page Footer
اگر میخواهید عباراتی در بالا و پایین تمام صفحات گزارش تکرار شوند میتوان از SectionReportHeader و SectionReportFooter این کتابخانه به صورت زیر استفاده کرد:
<xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>
<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام کاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>
دو نکته در اینجا حائز اهمیت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را میتوان در کدهای برنامه به صورت سفارشی اضافه کرد. بنابراین هر جایی که نیاز بود مقدار ثابتی از طریق کد نویسی به گزارش تزریق و اضافه شود میتوان از InlineDocumentValue استفاده کرد. برای مثال در کدهای ViewModel برنامه که در ادامه ذکر خواهد شد دو مقدار PrintDate و RptBy به صورت زیر تعریف و مقدار دهی شدهاند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
د) مشخص سازی ساختار تولیدی گزارش
<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از لیست محصولات در تاریخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>
<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>
<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>
<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قیمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع کل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
<Paragraph TextAlignment="Center" Margin="5">
در این گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هیچ"
FontWeight="Bold" /> محصول با جمع کل قیمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
در ابتدا توسط دو پاراگراف، عنوان گزارش و یک سطر زیر آن نمایش داده شدهاند. بدیهی است هر نوع شیء و فرمت مجاز در FlowDocument را میتوان در این قسمت نیز قرار داد.
سپس یک SectionDataGroup جهت نمایش لیست آیتمها اضافه شده و داخل آن یک جدول که بیانگر ساختار جدول نمایش رکوردهای گزارش میباشد، ایجاد گردیده است.
سه TableRowGroup در این جدول تعریف شدهاند.
TableRowGroup های اولی و آخری دو سطر اول و آخر جدول گزارش را مشخص میکنند (سطر عناوین ستونها در ابتدا و سطر جمع کل در پایان گزارش)
از TableRowGroup میانی برای نمایش رکوردهای مرتبط با نام جدول مورد گزارشگیری استفاده شده است. توسط TableRowForDataTable آن نام این جدول باید مشخص شود که در اینجا همان نام کلاس مدل برنامه است. به کمک InlineTableCellValue، خاصیتهایی از این کلاس را که نیاز است در گزارش حضور داشته باشند، ذکر خواهیم کرد. نکتهی مهم آن AggregateGroup ذکر شده است. توسط آن میتوان اعمال جمع، محاسبه تعداد، حداقل و حداکثر و امثال آنرا که در فایل InlineAggregateValue.cs سورس کتابخانه ذکر شدهاند، به فیلدهای مورد نظر اعمال کرد. برای مثال میخواهیم جمع کل قیمت را در پایان گزارش نمایش دهیم به همین جهت نیاز بود تا یک AggregateGroup را برای این منظور تعریف کنیم.
از این AggregateGroup در سومین TableRowGroup تعریف شده به کمک xrd:InlineAggregateValue جهت نمایش جمع نهایی استفاده شده است.
همچنین اگر نیاز بود در پایان گزارش اطلاعات بیشتری نیز نمایش داده شود به سادگی میتوان با تعریف یک پاراگراف جدید، اطلاعات مورد نظر را نمایش داد.
4) نمایش گزارش تهیه شده
نمایش این گزارش بسیار ساده است. View برنامه به صورت زیر خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>
تعریف ابتدایی RptGuiModel به صورت زیر است (جهت مشخص سازی مقادیر IsBusyIndicatorHidden و Document در حین بایندینگ اطلاعات):
using System.ComponentModel;
using System.Windows.Documents;
namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;
namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)
public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عملیات نمایش گزارش در یک ترد دیگر جهت قفل نشدن ترد اصلی برنامه
showReportAsync();
}
#endregion Constructors
#region Properties (1)
public RptGuiModel RptGuiModel { set; get; }
#endregion Properties
#region Methods (3)
// Private Methods (3)
private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });
return products;
}
private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;
var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};
var data = new ReportData();
// تعریف متغیرهای دلخواه و مقدار دهی آنها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحید");
// استفاده از یک سری اطلاعات آزمایشی به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());
var xps = reportDocument.CreateXpsDocument(data);
//انقیاد آن به صورت غیر همزمان در ترد اصلی برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود این مورد ضروری است زیرا بروز استثناء در یک ترد به معنای خاتمه آنی برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}
private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}
#endregion Methods
}
}
توضیحات:
برای اینکه حین نمایش گزارش، ترد اصلی برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer کمی نکته دار است:
- ترد تعریف شده باید از نوع STA باشد که در متد showReportAsync مشخص شده است.
- حین بایندیگ Document تولید شده توسط کتابخانهی گزارشگیری به خاصیت Document کنترل، حتما باید کل عملیات در ترد اصلی برنامه صورت گیرد که سورس کلاس DispatcherHelper را در فایل پیوست خواهید یافت.
کل عملیات این ViewModel در متد showReport رخ میدهد، ابتدا فایل گزارش بارگذاری میشود، سپس متغیرهای سفارشی مورد نظر تعریف و مقدار دهی خواهند شد. در ادامه یک سری داده آزمایشی تولید و به DataTables گزارش ساز اضافه میشوند. در پایان XPS Document متناظر آن تولید شده و به کنترل نمایشی برنامه بایند خواهد شد.
دریافت سورس این مثال
کدهای سمت سرور دریافت فایل PDF
در اینجا کدهای سمت سرور برنامه، نکتهی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت میدهد:
using Microsoft.AspNetCore.Mvc; namespace AngularTemplateDrivenFormsLab.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/assets/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
سرویس دریافت محتوای باینری در برنامههای Angular
برای اینکه HttpClient برنامههای Angular بتواند محتوای باینری را بجای محتوای JSON پیشفرض آن دریافت کند، نیاز است نوع خروجی سمت سرور آنرا به blob تنظیم کرد:
import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; @Injectable() export class DownloadPdfDataService { constructor(private httpClient: HttpClient) { } public getReport(): Observable<Blob> { return this.httpClient.get("/api/Reports/GetPdfReport", { responseType: "blob" }); } }
اصلاح 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 از سرور و نمایش آنها در یک برنامهی Angular
پس از این مقدمات، کامپوننتی که یک فایل PDF را از سمت سرور دریافت کرده و نمایش میدهد، چنین کدی را خواهد داشت:
import { DownloadPdfDataService } from "./../download-pdf-data.service"; import { WindowRefService } from "./../../core/window.service"; import { Component, OnInit } from "@angular/core"; import { DomSanitizer, SafeResourceUrl } from "@angular/platform-browser"; @Component({ templateUrl: "./view-pdf.component.html", styleUrls: ["./view-pdf.component.css"] }) export class ViewPdfComponent implements OnInit { private nativeWindow: Window; private pdfBlobUrl: string; sanitizedPdfBlobResourceUrl: SafeResourceUrl; constructor(private downloadService: DownloadPdfDataService, private windowRefService: WindowRefService, private sanitizer: DomSanitizer) { } ngOnInit() { this.nativeWindow = this.windowRefService.nativeWindow; this.downloadService.getReport().subscribe(pdfDataBlob => { console.log("pdfDataBlob", pdfDataBlob); const urlCreator = this.nativeWindow.URL; this.pdfBlobUrl = urlCreator.createObjectURL(pdfDataBlob); console.log("pdfBlobUrl", this.pdfBlobUrl); this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl); }); } }
<h1>Display PDF Files</h1> <div *ngIf="sanitizedPdfBlobResourceUrl"> <h4>using iframe</h4> <iframe width="100%" height="600" [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf"></iframe> <h4>using object</h4> <object [attr.data]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%" height="100%"></object> <h4>usin embed</h4> <embed [attr.src]="sanitizedPdfBlobResourceUrl" type="application/pdf" width="100%" height="100%"> </div>
- سپس مشترک متد getReport دریافت فایل PDF شده و اطلاعات نهایی آنرا به صورت pdfDataBlob دریافت میکنیم.
- این اطلاعات باینری را به متد createObjectURL ارسال کرده و آدرس موقتی این تصویر را در مرورگر بدست میآوریم.
- چون در این حالت Angular این URL را امن سازی میکند، یک چنین خروجی unsafe:blob بجای blob تولید خواهد شد که نمایش این مورد نیز توسط مرورگر سد میشود. برای رفع این مشکل، میتوان از سرویس DomSanitizer آن که به سازندهی کلاس تزریق شدهاست استفاده کرد:
this.sanitizedPdfBlobResourceUrl = this.sanitizer.bypassSecurityTrustResourceUrl(this.pdfBlobUrl);
اینبار یک چنین انتسابی به صورت مستقیم کار میکند که سه نمونهی این انتساب را به اشیاء iframe ،object و embed، در قالب فوق مشاهده میکنید.
افزودن دکمهی چاپ PDF به برنامه
پس از اینکه به this.pdfBlobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
printPdf() { const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = this.pdfBlobUrl; document.body.appendChild(iframe); iframe.contentWindow.print(); }
نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
showPdf() { this.nativeWindow.open(this.pdfBlobUrl); }
دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
downloadPdf() { const fileName = "test.pdf"; const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = this.pdfBlobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); }
این روش در مورد تدارک دکمهی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایلهای PDF نیست.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
زیرنویسهای فارسی قسمت آخر «Building Windows 8 Metro Apps in C# and XAML» را از اینجا میتونید دریافت کنید.
این قسمت برای کسانی که میخواهند مروری بر مفاهیم Binding موجود در WPF و سیلورلایت داشته باشند و همچنین تفاوتهای آنرا با نمونه موجود در WinRT بررسی کنند، بسیار مفید است. در کل سیستم Binding موجود در WinRT یک نمونه ساده شده آنچیزی است که در سیلورلایت موجود است (و البته سیلورلایت هم یک نمونه ساده شده WPF است!).
برای مثال خاصیت UpdateSourceTrigger موجود در عبارات Binding مرتبط با WPF و سیلورلایت فعلا در WinRT وجود ندارد.
تفاوت دیگر این است که هرچند اینترفیس INotifyPropertyChanged در WinRT هم وجود دارد اما نمونه مهیای در فضای نام جدیدی به نام windows.ui.xaml.data توسط WinRT شناسایی میشود و اگر کدهای سایر فناوریهای مشابه را به سیستم مترو تبدیل کنید، کار نخواهند کرد! چون این اینترفیس پیشتر در فضای نام System.ComponentModel تعریف شده بود و هنوز هم حضور دارد (فقط در حد یک تغییر سطر تعاریف فضاهای نام کفایت میکند).
یک تله دیگر هم در WinRT وجود دارد. در اینجا هم کلاسی به نام DependencyObject وجود دارد که ... معادل نمونه مشابه WPF و Silverlight نیست و XAML امکان تشخیص خودکار تغییرات خواص آنرا ندارد.
همچنین اینترفیس INotifyCollectionChanged مرتبط با ObservableCollection موجود در WPF و Silverlight در WinRT فعلا وجود خارجی ندارند (هرچند هنوز در خود دات نت فریم ورک وجود دارند، اما کار نمیکنند). به نظر قرار است تا قبل از ارائه نهایی ویندوز 8 در این مورد تصمیم گیری شود. اما در اینجا باید از IObservableVector استفاده کرد! (کلا این کلمه Vector ابراز وجود زاید طراحان سی++ مترو است! یعنی ما هنوز هم زندهایم و سی++ هنوز هم مهم است!)
نحوه گروه بندی اطلاعات نیز تغییر کرده است و باید منبع دادهای، اینترفیس جدید IGroupInfo را پیاده سازی کند. به علاوه CollectionViewSource آن نیز فعلا قابلیتهای جستجو و مرتب سازی موجود در WPF و سیلورلایت را ارائه نمیدهد.
سیلورلایت 5 و تاریخ شمسی
بنابراین از یادگیری سیلورلایت هیچ ضرری نخواهید کرد؛ حتی اگر مستقیما از آن استفاده نکنید. برای مثال کسی که با سیلورلایت آشنا هست راحت میتونه به WinRT ویندوز 8 کوچ کنه چون در ویندوز 8 برنامه نویسی برای WinRT «با» دات نت فقط به استفاده از XAML و یکی از زبانهای دات نتی خلاصه میشه. البته امکان استفاده از HTML خاص WinRT هم هست (که انتقال پذیر نیست و مخصوص کار با زیرساختهای WinRT یک سری اضافاتی رو داره) ولی کار با دات نت در این ویندوز برای تولید برنامههای مترو، پایه و اساسش همین سیلورلایت و WPF است.
من یک دوره WinRT رو زیرنویس دار کردم (^ ). نکته مهمش این است که در سراسر دوره عمده بحث بررسی تفاوتهای WinRT با سیلورلایت و WPF است. یعنی اساس یکی است و چیزی دور ریخته نشده. فقط یک سری موارد کم شده چند کنترل زیاد شده، یک سری مباحث برنامه نویسی غیرهمزمان به آن اضافه شده و از این مباحث.
- اضافه کردن اسمبلیها به صورت دستی به پروژه و تنظیم Build Action آنها به Embedded Resource
- تنظیم فایل csproj پروژه برای Embed کردن خودکار رفرنسهای پروژه در زمان Build
روش اول
روش دوم
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences"> <ItemGroup> <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" /> <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="'%(AssembliesToEmbed.Extension)' == '.dll'"> <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName> </EmbeddedResource> </ItemGroup> <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" /> </Target> <Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build"> <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" /> </Target>
همانطور که در تصویر بالا نیز مشاهده میکنید، اسمبلیهای ارجاعی برنامه TestApp به صورت Resource به آن اضافه شدهاند.
نحوه بارگذاری اسمبلیهای Embed شده
در پروژههای WPF، در OnStartup event کلاس App و در پروژههای WinForm در متد Main کلاس Program، قطعه کد زیر را وارد میکنیم:
private void App_OnStartup( object sender, StartupEventArgs e ) { AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly; var assembly = Assembly.GetExecutingAssembly(); foreach (var name in assembly.GetManifestResourceNames()) { if ( name.ToLower() .EndsWith( ".resources" ) || !name.ToLower() .EndsWith( ".dll" ) ) continue; EmbeddedAssembly.Load( name, name ); } } static Assembly OnResolveAssembly( object sender, ResolveEventArgs args ) { var fields = args.Name.Split( ',' ); var name = fields[0]; var culture = fields[2]; if ( name.EndsWith( ".resources" ) && !culture.EndsWith( "neutral" ) ) return null; return EmbeddedAssembly.Get( args.Name ); }
با استفاده از رویداد AssemblyResolve می توان اسمبلی Embed شده را در زمانیکه نیاز به آن است، بارگذاری کرد. کد مربوط به کلاس EmbeddedAssembly نیز به این صورت میباشد:
using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Cryptography; public static class EmbeddedAssembly { static Dictionary< string, Assembly > _dic; public static void Load( string embeddedResource, string fileName ) { if ( _dic == null ) _dic = new Dictionary< string, Assembly >(); byte[] ba; Assembly asm; var curAsm = Assembly.GetExecutingAssembly(); using ( var stm = curAsm.GetManifestResourceStream( embeddedResource ) ) { if ( stm == null ) return; ba = new byte[(int)stm.Length]; stm.Read( ba, 0, (int)stm.Length ); try { asm = Assembly.Load( ba ); _dic.Add( asm.GetName().Name, asm ); return; } catch { } } bool fileOk; string tempFile; using ( var sha1 = new SHA1CryptoServiceProvider() ) { var fileHash = BitConverter.ToString( sha1.ComputeHash( ba ) ) .Replace( "-", string.Empty ); tempFile = Path.GetTempPath() + fileName; if ( File.Exists( tempFile ) ) { var bb = File.ReadAllBytes( tempFile ); var fileHash2 = BitConverter.ToString( sha1.ComputeHash( bb ) ) .Replace( "-", string.Empty ); fileOk = fileHash == fileHash2; } else { fileOk = false; } } if ( !fileOk ) { File.WriteAllBytes( tempFile, ba ); } asm = Assembly.LoadFile( tempFile ); _dic.Add( asm.GetName().Name, asm ); } public static Assembly Get( string assemblyFullName ) { if ( _dic == null || _dic.Count == 0 ) return null; var name = new AssemblyName( assemblyFullName ).Name; return _dic.ContainsKey( name ) ? _dic[name] : null; } }
با استفاده از متد Load کلاس بالا، کل اسمبلیهایی که بارگذاری شدهاند در یک دیکشنری استاتیک نگهداری میشوند. ابتدا اسمبلیها را با استفاده از []byte بارگذاری میکنیم و در صورتیکه بارگذاری اسمبلی با خطایی مواجه شود، بارگذاری را با استفاده از فایل temp انجام میدهیم (که معمولا برای فایلهای unmanaged این مورد اتفاق میافتد).
با استفاده از متد Get که در زمان نیاز به یک اسمبلی توسط AssemblyResolve فراخوانی میشود، اسمبلی مربوطه از دیکشنری پیدا شده و برگشت داده میشود.
نکته ها
- در صورتیکه بخواهید فایلی را از Embed کردن خودکار (روش دوم) استثناء کنید، باید از Condition استفاده کنید:
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences"> <ItemGroup> <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" /> <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(AssembliesToEmbed.Filename)', '^((?!Microsoft).)*$')) And '%(AssembliesToEmbed.Extension)' == '.dll'"> <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName> </EmbeddedResource> </ItemGroup> <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" /> </Target> <Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build"> <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '^((?!Microsoft).)*$')) Or '%(Extension)' == '.xml'" /> </Target>
- در صورتیکه بعد از اجرای برنامه و یا اجرای به صورت دیباگ با خطای Stackoverflow مواجه شدید که به خاطر ارجاعات زیاد Resourceهای برنامه پیش میآید، کد زیر را به فایل AssemblyInfo، در پوشه Properties اضافه کنید:
[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.MainAssembly)]
- در صورتیکه پروژه شما از نوع Office Add-Ins باشد، باید در کد مربوط به AssemblyResolve را در فایل ThisAddIn.Designer.cs (در صورت عدم تغییر نام) به متد Initialize اضافه کنید و دستور بارگذاری را در متد ThisAddIn_Startup اضافه کنید. نکته خیلی مهم: در فایل csproj حتما در قسمت Condition باید اسمبلیهایی را که با نام Microsoft شروع میشوند، از Embed شدن استثناء کنید و در قسمت DeleteAllReferenceCopyLocalPaths مقدار "AfterTargets="VisualStudioForApplicationsBuild را قرار دهید (تا امکان Build پروژه برای شما باشد) و همچنین پسوند vsto را نیز نباید حذف کنید.
انقیاد در لیست List Binding
public static ObservableCollection<Employee> GetEmployees() { var employees = new ObservableCollection<Employee>(); employees.Add(new Employee() { Name = "Mahdi", Title = "Manager" }); employees.Add(new Employee() { Name = "Nima", Title = "Teacher" }); employees.Add(new Employee() { Name = "Rahim", Title = "Assistant" }); employees.Add(new Employee() { Name = "Saeed", Title = "Administrator" }); return employees; }
<ComboBox Name="President" ItemsSource="{Binding}" FontSize="30" Height="50" Width="550"> <ComboBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Title}" Margin="5,0,0,0"/> </StackPanel> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>
private ObservableCollection<Employee> employees; public MainWindow() { InitializeComponent(); employees = Employee.GetEmployees(); DataContext = employees; }
<Grid> <StackPanel Orientation="Horizontal"> <Slider Name="mySlider" Minimum="0" Maximum="100" Width="300"/> <TextBlock Margin="5" Text="{Binding Value,ElementName=mySlider}"/> </StackPanel> </Grid>
public DateTime BornDate { get { return _bornDate; } set { _bornDate = value; OnPropertyChanged(); } }
public static ObservableCollection<Employee> GetEmployees() { var employees = new ObservableCollection<Employee>(); employees.Add(new Employee() { Name = "Mahdi", Title = "Manager", BornDate = DateTime.Parse("2008/8/8") }); employees.Add(new Employee() { Name = "Nima", Title = "Teacher", BornDate = DateTime.Parse("2012/3/14") }); employees.Add(new Employee() { Name = "Rahim", Title = "Assistant", BornDate = DateTime.Parse("2009/11/18") }); employees.Add(new Employee() { Name = "Saeed", Title = "Administrator", BornDate = DateTime.Parse("2014/7/28") }); return employees; }
<ListBox ItemsSource="{Binding}" BorderThickness="1" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="100"/> <TextBlock Text="{Binding Title}" Width="100" Margin="5,0,0,0"/> <TextBlock Text="{Binding BornDate}" Margin="5,0,0,0"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
در زمانیکه در عملیات Data Binding نوع دادهی خصوصیت ما در Source (منبع داده) با نوع دادهی خصوصیت ما در target (کنترل یا View) متفاوت است، به یک مبدل در حین Binding نیاز داریم. این کار را از طریق یک کلاس که اینترفیس IValueConvertor را پیاده سازی کرده است، انجام میدهیم.
public class DateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { DateTime date = (DateTime)value; PersianCalendar pc = new PersianCalendar(); var persianDate = string.Format ($"{pc.GetYear(date)}/{pc.GetMonth(date)}/{pc.GetDayOfMonth(date)}"); return persianDate; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
xmlns:local="clr-namespace:DataConversion"
<Window.Resources> <local:DateConverter x:Key="MyConverter"/> </Window.Resources>
<TextBlock Text="{Binding BornDate,Converter={StaticResource MyConverter}}" Margin="5,0,0,0"/>