مطالب
تهیه گزارش در Blazor Wasm با استیمول ریپورت
 جهت تولید گزارش در Blazor Wasm، ابتدا آخرین نسخه‌ی استیمول سافت را از نیوگت دریافت کرده:
 Install-Package Stimulsoft.Reports.Blazor -Version 2021.2.4
سپس گزارشی را که با DataSource از نوع Business Objects ساخته‌ایم، در مسیر wwwroot/Reports قرار می‌دهیم. انتخاب نوع دیتاسورس اختیاری است و می‌توانیم از سایر دیتاسورس‌ها نیز استفاده کنیم.

جهت دسترسی به فایل گزارش، نیاز است فایل ریپورت، تبدیل به آرایه‌ای از بایت‌ها شود. به همین دلیل در Web Api یک متد را ساخته و در این متد، فایل گزارش را به آرایه تبدیل می‌کنیم:
[HttpGet]
[Route("GetReportFile/{fileName}")]
public async Task<IActionResult> GetReportFile(string fileName)
{
   var rootPath = _webHostEnverioment.WebRootFileProvider.GetDirectoryContents("/")
                     .FirstOrDefault(x => x.Name == "Reports")?.PhysicalPath;
   var path = Path.Combine(rootPath!, fileName);
   var bytes = await System.IO.File.ReadAllBytesAsync(path);
   return Ok(bytes);
}
و سپس در فایل Razor بوسیله HttpClient گزارش را نمایش می‌دهیم:
@page "/"
@using Stimulsoft.Base
@using Stimulsoft.Report
@using Stimulsoft.Report.Blazor
@inject HttpClient Http
<StiBlazorViewer Report="@report" />

@code
{
    private StiReport report;
    protected override async Task OnInitializedAsync()
    {
        //Create empty report object
        report = new StiReport();
        //Load report template
        // var reportBytes = await Http.GetByteArrayAsync("Reports/Report.mrt");
        report.RegBusinessObject("MyList", GetBusinessObject());
        var uri = $"/api/Report/GetReportFile/Report.mrt";
        var reportFile=await Http.GetFromJsonAsync<byte[]>(uri);
      //  var reportFile = await Http.GetByteArrayAsync("Report.mrt");
        report.Load(reportFile);
        await report.Dictionary.SynchronizeAsync();

    }

    public class BusinessEntity
    {
        public string Name { get; set; }
        public string Alias { get; set; }
        public BusinessEntity(string name, string alias)
        {
            Name = name;
            Alias = alias;
        }
    }

    private System.Collections.ArrayList GetBusinessObject()
    {
        var list = new System.Collections.ArrayList();
        list.Add(new BusinessEntity("ali", "alias1"));
        list.Add(new BusinessEntity("reza", "alias2"));
        return list;
    }
}

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmReport.zip   
نظرات مطالب
استفاده از Interop.word برای جایگزین کردن مقادیر در تمامی فایل (Footer - Header - ... )
سلام؛ من ازهمین روش شما استفاده کردم چند وقت پیش به وسیله  bookmark:
        private Document oDoc;
        public void createdoc1()
        {
            var realpath="~/template";
            var filePath = Path.Combine(HttpContext.Current.Server.MapPath("~/template"), Lcourseid.Text + ".doc");
            var oWordApplication = new Application();
            DirectoryInfo dir = new DirectoryInfo(Server.MapPath(realpath));
            foreach (FileInfo files in dir.GetFiles())
            {
                files.Delete();
            }
            // To invoke MyMethod with the default argument value, pass  
            // Missing.Value for the optional parameter.
            object missing = System.Reflection.Missing.Value;

            //object fileName = ConfigurationManager.AppSettings["DocxPath"];@"C:\DocXExample.docx";
            string fileName = @"D:\template1.dot";
            //string fileName1 = @"D:\sss.doc";
            object newTemplate = false;

 object docType = 0;
            object isVisible = true;

            //System.Reflection.Missing.Value is used here for telling that method to use default parameter values when method execution
            oDoc = oWordApplication.Documents.Open(fileName, newTemplate, docType, isVisible, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing,
                ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);
           // usable in earlier versions of Microsoft Word v2003  v11
          //  if(Convert.ToInt16(oWordApplication.Version) >=11)
            {
             //Sets or returns a Boolean that represents whether a document is being viewed in reading layout view.
              oDoc.ActiveWindow.View.ReadingLayout = false;
           }

            //The active window is the window that currently has focus.If there are no windows open, an exception is thrown.
            //microsoft.office.tools.word.
            oDoc.Activate();

            if (oDoc.Bookmarks.Exists("Title"))
            {
                oDoc.Bookmarks["Title"].Range.Text = "Test Field Entry from webform";
                oDoc.Bookmarks["Address"].Range.Text = "Address Field Entry from webform";
            }

            oDoc.SaveAs(filePath, ref missing);
            oWordApplication.Documents.Close(ref missing, ref missing, ref missing);
            //oWordApplication.Quit(ref SaveChanges, ref missing, ref missing, ref missing);
            ProcessRequest(filePath, Lcourseid.Text);
ولی این روش مشکلاتی هم داره. اول اینکه باید روی سرور تنظیمات خاصی رو انجام بدی. البته از تنظیمات منظور تنظیمات دسترسی کاربران هست. ولی استفاده از داک ایکس بیشتر استقبال میشه چون دردسرش کمتره.
مطالب
آشنایی با نحوه‌ی وهله سازی کنترلرها در ASP.NET MVC با ساخت یک Controller Factory سفارشی
یکی از مزایای مهم فریم ورک ASP.NET MVC، توسعه پذیری کنترلرهای آن است. با مرور قسمت‌هایی از مسیر پردازش درخواست که منجر به اجرای یک اکشن متد می‌شود، شروع می‌کنیم و روش‌های مختلفی را که می‌توان بر روی این پردازش، کنترل داشت، بررسی می‌کنیم. شکل ذیل مسیر یک درخواست را مابین کامپوننت‌های مختلف فریم ورک نشان می‌دهد:
 
 

Controller Factory و Action Invoker وظیفه‌ای مطابق نامشان را عهده دار هستند. اولی برای وهله سازی کنترلرهای مرتبط با درخواست و دومی برای پیدا کردن و تریگر نمودن یک اکشن متد به کار گرفته می‌شوند. فریم ورک MVC پیاده سازی پیش فرضی را از این دو کامپوننت، به صورت توکار دارد. در طی مقالاتی نحوه‌ی کنترل کردن رفتار پیش فرض این Controller Factory و هم نحوه‌ی جایگزین کرن کامل این کامپوننت را بررسی می‌کنیم.

ابتدا پروژه‌ی جدیدی را از نوع MVC و با الگوی Empty به نام ControllerExtensibility ایجاد می‌کنیم. در پوشه‌ی Models یک فایل را به نام Result.cs ساخته و از آن برای معرفی کلاس Result مطابق کدهای ذیل استفاده می‌کنیم:
namespace ControllerExtensibility.Models
{
    public class Result
    {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
    }
}
در مسیر /Views/Shared ویویی را به نام Result.cshtml اضافه می‌کنیم. این ویویی است که در این مثال، همه‌ی اکشن متدهای کنترلرهایمان، آن را رندر خواهند کرد:
@model ControllerExtensibility.Models.Result
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Result</title>
</head>
<body>
<div>Controller: @Model.ControllerName</div>
<div>Action: @Model.ActionName</div>
</body>
</html>
در خط اول، مدل ویو را از نوع کلاس Result تعیین کرده‌ایم.
دو کنترلر را نیز حاوی کدهای زیر ایجاد می‌کنیم:

کنترلر product
using ControllerExtensibility.Models;
using System.Web.Mvc;
namespace ControllerExtensibility.Controllers
{
    public class ProductController : Controller
    {
        public ViewResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Product",
                ActionName = "Index"
            });
        }
        public ViewResult List()
        {
            return View("Result", new Result
            {
                ControllerName = "Product",
                ActionName = "List"
            });
        }
    }
}

کنترلر customer

using System.Web.Mvc;
namespace ControllerExtensibility.Controllers
{
    public class CustomerController : Controller
    {
        public ViewResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Customer",
                ActionName = "Index"
            });
        }
        public ViewResult List()
        {
            return View("Result", new Result
            {
                ControllerName = "Customer",
                ActionName = "List"
            });
        }
    }
}
اکشن‌های این دو کنترلر حاوی کد خاصی نبوده و صرفا ویوی Result.cshtml را صدا می‌زنند. ولی در این مرحله این همه‌ی آن چیزی است که برای نشان دادن نحوه‌ی سفارشی کردن کنترلرها بدان نیاز داریم.
ایجاد یک Controller Factory سفارشی بهترین راه برای درک نحوه‌ی وهله سازی کنترلر‌ها توسط MVC است. ولی این کار صرفا جنبه‌ی آموزشی داشته و در یک پروژه‌ی واقعی این نوع پیاده سازی‌ها پیشنهاد نمی‌شود؛ زیرا راه‌های مفیدتر و ساده‌تری با پیاده سازی توکار Controller Factory وجود دارند.
Controller Factory‌ها با پیاده سازی اینترفیس IControllerFactory معرفی می‌شوند. کدهای این اینترفیس را در ذیل می‌بینید:
using System.Web.Routing;
using System.Web.SessionState;
namespace System.Web.Mvc
{
    public interface IControllerFactory
    {
        IController CreateController(RequestContext requestContext,
        string controllerName);
        SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext,
        string controllerName);
        void ReleaseController(IController controller);
    }
}
پوشه‌ای را به نام Infrastructure ساخته و فایلی را به نام CustomControllerFactory.cs ، حاوی کدهای زیر اضافه کنید:
using System;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using ControllerExtensibility.Controllers;

namespace ControllerExtensibility.Infrastructure
{
    public class CustomControllerFactory : IControllerFactory
    {
        public IController CreateController(RequestContext requestContext,
            string controllerName)
        {
            Type targetType = null;
            switch (controllerName)
            {
                case "Product":
                    targetType = typeof (ProductController);
                    break;
                case "Customer":
                    targetType = typeof (CustomerController);
                    break;
                default:
                    requestContext.RouteData.Values["controller"] = "Product";
                    targetType = typeof (ProductController);
                    break;
            }
            return targetType == null
                ? null
                : (IController) DependencyResolver.Current.GetService(targetType);
        }

        public SessionStateBehavior GetControllerSessionBehavior(RequestContext
            requestContext, string controllerName)
        {
            return SessionStateBehavior.Default;
        }

        public void ReleaseController(IController controller)
        {
            IDisposable disposable = controller as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }
    }
}
مهمترین متد کدهای فوق، CreateController است که فریم ورک، بر حسب نیاز، جهت سرویس دهی به درخواست واصله آن را صدا خواهد زد. پارامتر ورودی این متد، شیء RequestContext است که جزئیاتی در خصوص درخواست واصله را در اختیار factory خواهد گذاشت. همچنین یک رشته که نام کنترلر را بر حسب URL واصله تعیین می‌کند:
 

نام

نوع

توضیحات

HttpContext

HttpContextBase

حاوی اطلاعاتی در خصوص درخواست است.

RouteData

RouteData

حاوی اطلاعاتی در خصوص Rout است که با درخواست رسیده همخوانی دارد.

 
یکی از دلایلی که عنوان شد Controller factory سفارشی بدین روش در یک پروژه‌ی عملی به کار گرفته نشود این است که یافتن کلاس‌هایی از نوع Controller در سراسر برنامه و وهله سازی آنها کار دشواری است. چرا که لازم خواهد بود بتوانید به صورت پویا کنترلر را مکان یابی کرده و بین کلاس‌های هم نام در دیگر فضاهای نام تمییز قائل شوید و خطاهای محتمل در حین وهله سازی را کنترل کنید.
در این مثال تنها دو کنترلر داریم و آنها را به صورت مستقیم در Controller Factory وهله سازی می‌کنیم که در یک پروژه‌ی واقعی مطلوب نیست. ولی آنچه را که این روش آشکار‌تر می‌سازد، انعطاف پذیری بالای فریم ورک MVC است که دست ما را برای نفوذ و دخل و تصرف در اعمال و رفتاریهای پیش فرض خود باز گذاشته است و برای مثال در مباحث تزریق وابستگی‌ها و تنظیمات ابتدایی IoC Containers کاربرد دارد.
متد CreateController لازم است وهله‌ای از کلاسی که اینترفیس IController را پیاده سازی کرده برگرداند؛ در غیر اینصورت کار با خطا متوقف خواهد شد. لذا برای زمانی که درخواست کاربر، هیچ کدام از کنترلر‌ها را مشمول عنایت قرار نمی‌دهد، باید چاره‌ای اندیشیده شود.
می‌توان آن را به کنترلر خاصی که پیغام خطایی را رندر می‌کند، هدایت کنیم. به عبارت بهتر باید درخواست را به کنترلری که مطمئن هستیم وجود دارد (اصطلاحا کنترلر جانشین) هدایت نماییم. همان طور که در کد فوق در قسمت default می‌بینید:
default:
requestContext.RouteData.Values["controller"] = "Product";
targetType = typeof(ProductController);
break;
در صورت عدم تطابق با هیچ کدام از حالات تعیین شده، درخواست را به کنترلر ProductController جهت رسیدگی هدایت کرده‌ایم.
در MVC انتخاب ویوی مناسب، بر حسب مقدار RouteData.Values صورت می‌گیرد؛ نه نام کلاس Controller و این سبب خواهد شد فریم ورک، ویوهای مرتبط با کنترلر جانشین شده‌ی توسط ما را جستجو کند و نه کنترلری که کاربر از طریق URL ورودی آن را درخواست کرده است.
لذا Controller Factory صرفا وظیفه مپ کردن درخواست‌های واصله به کنترلر‌ها را ندارد، بلکه توانایی دخل و تصرف در درخواست واصله بر حسب مورد را نیز خواهد داشت.
در نهایت هم نحوه‌ی استفاده از DependencyResolver را برای وهله سازی کلاس‌های کنترلر می‌بینید. متد استاتیک Current یک پیاده سازی از اینترفیس IDependencyResolver را که حاوی متد GetService است، برگشت داده و سپس یک شیء System.Type را به عنوان ورودی گرفته و یک وهله‌ی ساخته شده‌ی از آن را به عنوان خروجی برمی‌گرداند.
متد GetControllerSessionBehavior نیز توسط MVC جهت تعیین اینکه Session data برای کنترلر نیاز است یا خیر به کار گرفته می‌شود.
متد ReleaseController نیز هر گاه به شیء کنترلر ساخته شده در متد CreateController دیگر نیازی نبود، صدا زده خواهد شد. در کدهای ما ابتدا بررسی می‌شود آیا اینترفیس IDisposable توسط کلاس، پیاده سازی شده است یا خیر؟ اگر بلی متد Dispose آن جهت آزاد سازی منابعی که می‌توانند آزاد شوند، صدا زده می‌شود.
جهت ثبت Controller  Factory ساخته شده در متد Application_Start موجود در فایل global.asax.cs بوسیله کلاس ControllerBuilder و مطابق کدهای ذیل عمل می‌نماییم:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Routing;
using ControllerExtensibility.Infrastructure;
namespace ControllerExtensibility
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            ControllerBuilder.Current.SetControllerFactory(new
            CustomControllerFactory());
        }
    }
}
پس از ثبت به شیوه‌ی فوق، controller factory ساخته شده، مسئول هندل کردن تمامی درخواست‌های واصله‌ی به برنامه خواهد بود. پس از اولین اجرا، مرورگر ریشه‌ی سایت را هدف قرار خواهد داد که توسط سیستم مسیر یابی به کنترلر Home، نگاشت شده و بر اساس تعاریف و کدهای ما، چون با هیچ کدام از کنترلرهای Product و Customer تطابق نخواهد داشت، به کنترلر جایگزین تنظیم شده، یعنی Product هدایت خواهد شد.


 
مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت سوم
در قسمت قبل، محیط توسعه نرم افزار مد نظرمان را ایجاد کردیم و توانستیم پروژه پیش فرض Xamarin forms را بیلد کنیم. حالا قصد داریم تا یک مثال ساده را با هم بررسی کنیم و آن را بر روی ویندوز تست کنیم. در قسمت بعدی نیز همین مثال ساده را بر روی Android و در قسمت بعدتر نیز بر روی iOS تست می‌کنیم. پس از اطمینان از اینکه امکان تست برنامه را بر روی هر سه پلتفرم یافته‌اید، بر روی آموزش موارد بیشتری از Xamarin Forms تمرکز می‌کنیم.
برای شروع، Xaml Live را از Visual Studio Marketplace دانلود کنید.
سپس در صورتیکه ابزار git را ندارید، آن را نیز دانلود و نصب کنید و بعد دستور git clone https://github.com/ysmoradi/XamApp را در Command line وارد کنید. دقت کنید پروژه را در جایی Clone نکنید که مسیر فولدر آن طولانی شود. این پروژه، یک پروژه آماده برای تست و تغییر است و ما برای بررسی نحوه اجرا آن در UWP - Android - iOS‌ از آن استفاده می‌کنیم. خود کدها در جلسات بعدتر بررسی خواهند شد. تنها چیزی که الآن اهمیت دارد اطمینان از راه اندازی شدن محیط توسعه نرم افزار شما به بهترین شکل ممکن است.
   
با ساختار پروژه و جزئیات آن در گذر زمان بیشتر آشنا می‌شویم، ولی به صورت کلی این Solution دارای 4 پروژه است:
XamApp - XamApp.Android - XamApp.iOS - XamApp.UWP
- در XamApp، تقریبا تمامی پروژه نوشته می‌شود. این مورد شامل منطق برنامه است که با CSharp نوشته می‌شود و ظاهر برنامه که با XAML نوشته می‌شود. اگر چه که می‌توان ظاهر برنامه را نیز با CSharp زد، انجام این کار به هیچ وجه توصیه نمی‌شود. در مورد Xaml نیز بعد از راه اندازی این مثال در Windows-Android-iOS صحبت خواهیم کرد.
- XamApp.Android پروژه‌ای است که اگر Set as start up project شود، به شما اجازه تست کدهای داخل XamApp را بر روی گوشی یا Emulator‌های اندرویدی می‌دهد. همچنین از طریق این پروژه می‌توانید پابلیش apk را بگیرید و به امکانات پایه Android مانند Activity - Fragment - Android XML - Intent و ... در همان زبان CSharp دسترسی داشته باشید. استفاده از این پروژه، مطلب قسمت بعدی این دوره آموزشی است.
- XamApp.iOS پروژه‌ای است که اگر Set as start up project شود، به شما اجازه تست کدهای داخل XamApp را بر روی گوشی یا Emulator‌های iOS ای می‌دهد. همچنین از طریق این پروژه می‌توانید پابلیش ipa را بگیرید و به امکانات پایه iOS مانند Delegate - Storyboard و ... در همان زبان CSharp دسترسی داشته باشید. در آموزش بعد از آموزش Android، به iOS نیز خواهیم پرداخت.
- XamApp.UWP پروژه‌ای است که اگر Set as start up project شود، به شما اجازه تست کدهای داخل XamApp را بر روی ویندوز خودتان می‌دهد. همچنین از طریق این پروژه می‌توانید پابلیش appxbundle یا msi را بگیرید و به امکانات پایه Windows در همان زبان CSharp دسترسی داشته باشید. UWP مخفف Universal windows platform است.

برای شروع پروژه‌های XamApp.Android و XamApp.iOS را Unload کنید، زیرا در این قسمت با آنها کاری نداریم. همچنین پروژه XamApp.UWP را Set as start up project کنید. 
فقط برای یکبار از منوی Tools در ویژوال استدیو، Options را باز کنید و در قسمت جستجوی آن، عبارت Intellitrace را بنویسید و اگر چیزی پیدا شد، تیک Enable Intellitrace را بردارید تا غیر فعال شود. همچنین مجدد Suppress JIT optimization را جستجو کنید و تیک آن را بزنید تا فعال شود.
دکمه F5 را بزنید و برنامه را اجرا کنید. یک دکمه می‌بینید که Text آن عبارت + است. اگر بر روی آن کلیک کنید، می‌بینید که متن Label بالای آن می‌شود Button tapped 1 time
فایل HelloWorldView.xaml را در فولدر Views در پروژه XamApp، پیدا کنید و نگاهی به کد Label و Button بیاندازید که درون StackLayout هستند و StackLayout خود داخل ContentPage است که در نتیجه صفحه اول ما کل فضایی را که دارد، به StackLayout اختصاص یافته‌است.
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}" />
        <Button Command="{Binding IncreaseStepsCountCommand}" Text="+" />
    </StackLayout>
StackLayout نوعی Layout ساده‌است که بسته به تنظیم Orientation اش، می‌تواند Vertical یا Horizontal باشد. آیتم‌های داخل‌اش را که در این مثال Label و Button هستند، یا عمودی یا افقی می‌چیند که پیش فرض‌اش عمودی است. Horizontal Options و Vertical Options اش هم که هر دو Center هستند باعث می‌شود آیتم‌ها دقیقا در وسط StackLayout بنشینند. چون تمامی فضای ContentPage به StackLayout اختصاص یافته‌است، عملا Label و Button در وسط برنامه ظاهر می‌شوند.
Label ما دارای Text ای است که به StepsCount متصل یا Bind شده است ( به کمک Binding StepsCount).
StepsCount عددی است که در ابتدا صفر است و با کلیک دکمه، مقدار آن یکی یکی افزایش می‌یابد. این کد در HelloWorldViewModel.cs و به زبان CSharp نوشته شده‌است. StringFormat نیز در اینجا عملکردی معادل StringFormat در CSharp را دارد.
‌Button دارای Command ای است که به متدی در CSharp به نام IncreaseStepsCount متصل شده‌است.
حال نگاهی به کد CSharp بیندازیم:
    public class HelloWorldViewModel : BitViewModelBase
    {
        public int StepsCount { get; set; }

        public BitDelegateCommand IncreaseStepsCountCommand { get; set; }

        public HelloWorldViewModel()
        {
            IncreaseStepsCountCommand = new BitDelegateCommand(IncreaseStepsCount);
        }

        async Task IncreaseStepsCount()
        {
            StepsCount += 1;
        }
    }
ظاهر برنامه در فایل Xaml ای نوشته شده به نام HelloWorldView.xaml و منطق برنامه در کلاسی است به نام HelloWorldViewModel که این View و ViewModel انتهای نام این دو، از معماری MVVM یا Model - View - View Model می‌آید که در سال 2006~2007 و با معرفی WPF کم کم معروف شد و ما نیز از آن در این مثال داریم استفاده می‌کنیم.
StepsCount که در View به Text آن Label وصل شده بود، در CSharp یک Property از جنس int است. Command ما با نام IncreaseStepsCountCommand به متدی وصل شده‌است که کارش اضافه کردن یکی یکی StepsCount است.

در حالت عادی اگر بخواهید این برنامه را تغییر دهید که مثلا به جای یکی یکی بالا بردن StepsCount، آن را یکی یکی کم کند، ابتدا برنامه را Stop می‌کنید و سپس در Xaml برای Button مربوطه، Text را از + به - تغییر می‌دهید. همچنین کد CSharp را نیز عوض می‌کنید که می‌شود:
StepsCount -= 1
و مجددا F5 را می‌زنید. این روش قطعا خیلی Productive نیست و زمان زیادی را از شما می‌گیرد. شما می‌توانید با Break کردن اجرای برنامه به تغییر کدهای CSharp بپردازید. همچنین بدون Break کردن می‌توانید کدهای Xaml را تغییر دهید و به این روی، خیلی سریع‌تر پروژه را پیش ببرید.
با مشاهده این ویدئو می توانید درک بهتری از عملکرد Edit & continue داشته باشید. دقت کنید که در زمان تغییر ظاهر و منطق، اگر مثلا عدد، تا 17 افزایش داده شده بود برای تست، روی 17 می‌ماند و به صفر بر نمی‌گردد. در واقع کل برنامه Reload نمی‌شود و این تفاوت Edit & continue با Hot reload موجود در سایر ابزارهاست.

همچنین با کوچک و بزرگ کردن برنامه اجرا شده به سایز گوشی‌ها و تبلت‌های مختلف عملا می‌توانید برنامه را در سایزهای مختلف تست کنید. توجه داشته باشید که در Xamarin Forms مقدار دهی به طول و عرض و ... در تمامی پلتفرم‌ها و Device‌ها فارغ از Resolution یکسان است و در همه جا Width=64 عملا به یک سانتی متر اشاره دارد. علاوه بر این بدون اینکه صفحه مانیتور شما Touch باشد، می‌تواند حتی Touch را نیز تست کنید که برای این کار می‌توانید از Simulator استفاده کنید. به این صورت که به جای Local Machine گزینه Simulator را انتخاب می‌کنید.
 

 

برای پابلیش پروژه نیز می‌توانید از آموزش‌های بر روی وب استفاده کنید که شامل ارائه برنامه به استفاده کنندگان با یا بدون Microsoft Store است که از فرمت نه چندان جالب appxbundle استفاده می‌کند و ما از این آموزش عبور می‌کنیم و به ذکر این نکته بسنده می‌کنیم که نسخه بعدی Visual Studio 2017 یعنی 15.9 قابلیت ساختن msix یا Windows installer را نیز دارد که از هر چیزی بهتر است و برای پابلیش بهتر است تا ارائه نسخه Stable بعدی ویژوال استودیو که احتمالا در طی کمتر از یک ماه دیگر ارائه می‌شود، صبر کنید. دقت کنید علاوه بر کامپیوتر، لپ تاپ و تبلت‌های ویندوزی، برنامه‌ی شما بر روی XBox نیز می‌تواند کار کند.

در قسمت بعدی، همین پروژه را بر روی Android نیز اجرا می‌کنیم.

نظرات مطالب
iTextSharp و استفاده از قلم‌های محدود فارسی
روش متداول، نصب Adobe reader در سمت کاربر است. اکتیوایکس آن سال‌ها است که با اکثر مرورگرها کار می‌کند و امکان مشاهده فایل pdf را درون خود مرورگر به صورت یکپارچه میسر کرده.
https://www.dntips.ir/2011/07/pdf-winforms-wpf.html
مطالب
تنظیمات CORS در ASP.NET Core
برنامه‌های امروزی، ممکن است به چندین Web API مستقل، تبدیل شده و سپس برنامه‌هایی (Front-ends) جدای از آن‌ها برای کار با آن‌ها ایجاد شوند. بنابراین این وظیفه‌ی برنامه‌‌های Web API است که مطمئن شوند کلاینت‌ها قادر به تعامل با آن‌ها هستند. CORS استانداردی است که یک چنین امکانی را مهیا می‌کند.



CORS چیست؟

CORS و یا cross origin resource sharing، یک مکانیزم امنیتی است که در تمام مرورگرهای جدید جهت جلوگیری از ارسال اطلاعات یک وب سایت، به وب سایتی دیگر، درنظر گرفته شده‌است. درخواست دسترسی به یک منبع (Resource) مانند تصویر، داده و غیره، خارج از سرآغاز آن، یک درخواست cross-origin نامیده می‌شود. برای مدیریت یک چنین درخواست‌هایی، استانداردی به نام CORS طراحی شده‌است. می‌توان به آن مانند نگهبانی یک ساختمان نگاه کرد که تا مجوز خاصی به آن‌ها ارائه نشود، امکان دسترسی به منابع ساختمان را صادر نخواهند کرد.


Origin چیست؟

سرآغاز یک درخواست از سه قسمت تشکیل می‌شود:
- Protocol/Scheme مانند HTTP/HTTPS
- Host مانند نام دومین
- شماره پورت مانند 443 برای پروتکل HTTPS  یا پورت 80 برای HTTP که عموما هر دو مورد به علت پیش‌فرض بودن، ذکر نمی‌شوند

بنابراین URLای مانند https://www.dntips.ir یک Origin را مشخص می‌کند. در اینجا به تمام منابعی که از این سرآغاز شروع می‌شوند و سه قسمت یاد شده‌ی آن‌ها یکی است، same-origin گفته می‌شود.
در این حالت اگر منابعی به URLهایی مانند https://www.dntips.ir (پروتکل متفاوت) و یا https://github.com (با host متفاوت) اشاره کنند، به آن‌ها Different-Origin گفته خواهد شد.


سیاست امنیتی Same-Origin چیست؟

سیاست امنیتی Same-Origin که به صورت توکار در تمام مرورگرهای امروزی تعبیه شده‌است، مانع تعامل سرآغازهایی متفاوت، جهت جلوگیری از حملات امنیتی مانند Cross-Site Request Forgery یا CSRF می‌شود. این سیاست امنیتی بسیار محدود کننده‌است و برای مثال مانع این می‌شود که بتوان توسط کدهای JavaScript ای، درخواستی را به سرآغاز دیگری ارسال کرد؛ حتی اگر بدانیم درخواست رسیده از منبعی مورد اطمینان صادر شده‌است. برای مثال اگر سایت جاری یک درخواست Ajax ای را به https://anotherwebsite.com/api/users ارسال کند، چون قسمت host مربوط به origin آن‌ها یکی نیست، این عملیات توسط مرورگر غیرمجاز شمرده شده و مسدود می‌شود.


چگونه می‌توان از تنظیمات CORS، برای رفع محدودیت‌های سیاست‌های دسترسی Same-Origin استفاده کرد؟

استاندارد CORS تعدادی header ویژه را جهت تبادل اطلاعات بین سرور و مرورگر مشخص کرده‌است تا توسط آن‌ها بتوان منابع را بین سرآغازهای متفاوتی به اشتراک گذاشت. در این حالت ابتدا کلاینت درخواستی را به سمت سرور ارسال می‌کند. سپس سرور پاسخی را به همراه هدرهای ویژه‌ای که مشخص می‌کنند به چه منابعی و چگونه می‌توان دسترسی یافت، به سمت کلاینت ارسال خواهد کرد. اکثر این هدرها با Access-Control-Allow شروع می‌شوند:
• Access-Control-Allow-Origin
در آن لیست سرآغازهایی را که سرور مایل است منابع خود را با آن‌ها به اشتراک بگذارد، مشخص می‌شوند. در حالت توسعه‌ی برنامه می‌توان از مقدار * نیز برای آن استفاده کرد تا هر سرآغازی بتواند به منابع سرور دسترسی پیدا کند. اما از استفاده‌ی این مقدار سراسری و کلی، در حالت انتشار برنامه خودداری کنید.
• Access-Control-Allow-Headers
• Access-Control-Allow-Methods
• Access-Control-Allow-Credentials


درخواست‌های ویژه‌ی Pre-Flight

در بسیاری از موارد، مرورگر زمانیکه تشخیص می‌دهد درخواست صادر شده‌ی از طرف برنامه، قرار است به یک Origin دیگر ارسال شود، خودش یک درخواست ویژه را به نام Pre-Flight و از نوع OPTIONS به سمت سرور ارسال می‌کند. علت آن این است که سرورهای قدیمی، مفهوم CORS را درک نمی‌کنند و در برابر درخواست‌های ویژه‌ای مانند Delete که از سرور جاری صادر نشده باشند، مقاومت می‌کنند (صرفا درخواست‌های Same-Origin را پردازش می‌کنند). به همین جهت است که اگر به برگه‌ی network ابزارهای توسعه دهندگان مرورگر خود دقت کنید، درخواست‌های cross-origin به نظر دوبار ارسال شده‌اند. بار اول درخواستی که method آن، options است و در خواست دوم که درخواست اصلی است و برای مثال method آن put است. این درخواست اول، Pre-Flight Request نام دارد. هدف آن این است که بررسی کند آیا سرور مقصد، استاندارد CORS را متوجه می‌شود یا خیر و آیا اینقدر قدیمی است که صرفا درخواست‌های Same-Origin را پردازش می‌کند و مابقی را مسدود خواهد کرد. اگر سرور درخواست Pre-Flight را درک نکند، مرورگر درخواست اصلی را صادر نخواهد کرد.



البته درخواست‌های pre-flight بیشتر در حالت‌های زیر توسط مرورگر صادر می‌شوند:
- اگر متد اجرایی مدنظر، متدی بجز GET/POST/HEAD باشد.
- اگر content type درخواست‌های از نوع POST، چیزی بجز application/x-www-form-urlencoded, multipart/form-data و یا text/plain باشند.
- اگر درخواست، به همراه یک هدر سفارشی باشد نیز سبب صدور یک pre-flight request خواهد شد.

بنابراین در برنامه‌های SPA خود که اطلاعات را با فرمت JSON به سمت یک Web API ارسال می‌کنند (و Origin آن‌ها یکی نیست)، حتما شاهد درخواست‌های Pre-Flight نیز خواهید بود.


تنظیمات CORS در ASP.NET Core

اکنون که با مفهوم CORS آشنا شدیم، در ادامه به نحوه‌ی انجام تنظیمات آن در برنامه‌های ASP.NET Core خواهیم پرداخت.
تنظیمات ابتدایی CORS در فایل Startup.cs و متد ConfigureServices آن انجام می‌شوند:
public void ConfigureServices(IServiceCollection services) 
{ 
    // Add service and create Policy with options 
    services.AddCors(options => 
    { 
        options.AddPolicy("CorsPolicy", 
            builder => builder.AllowAnyOrigin() 
            .AllowAnyMethod() 
            .AllowAnyHeader() 
            .AllowCredentials() ); 
    }); 
     
    // Make sure MVC is added AFTER CORS 
    services.AddMvc(); 
}
ابتدا توسط متد AddCors، سرویس‌های مرتبط، به سیستم تزریق وابستگی‌های ASP.NET Core اضافه خواهند شد و سپس در قسمت تنظیمات آن می‌توان همان هدرهای Access-Control-Allow را که پیشتر در مورد آن‌ها بحث کردیم، تعریف و مقدار دهی کرد. برای مثال تنظیم AllowAnyOrigin، همان استفاده‌ی از مقدار * در هدر Access-Control-Allow-Origin تولیدی است. باید دقت داشت که تنظیمات این سرویس‌ها باید پیش از افزودن سرویس‌های MVC انجام شوند.
در اینجا CorsPolicy که به عنوان پارامتر مشخص شده‌است، نام این سیاست دسترسی سفارشی است و در قسمت‌های مختلف برنامه می‌توان ارجاعاتی را به این نام تعریف کرد.

و برای تعریف سرآغازی خاص و یا متدی مشخص، می‌توان به صورت زیر عمل کرد (ذکر صریح WithOrigins برای حالت توزیع برنامه مناسب است و از دیدگاه امنیتی، استفاده‌ی از AllowAnyOrigin کار صحیحی نیست ):
services.AddCors(options =>
{
    options.AddPolicy("AllowOrigin",
            builder => builder.WithOrigins("http://localhost:55294")
                              .WithMethods("GET", "POST", "HEAD"));
});

پس از تنظیم سیاست دسترسی مدنظر، اکنون نوبت به اعمال آن است. اینکار در متد Configure فایل Startup.cs انجام می‌شود:
public void Configure(IApplicationBuilder app) 
{ 
    // ... 
 
    app.UseCors("CorsPolicy"); 
     
    // ... 
     
    app.UseMvc(routes => 
    { 
        routes.MapRoute( 
            name: "default", 
            template: "{controller=Home}/{action=Index}/{id?}"); 
    }); 
}
در اینجا متد UseCors که میان‌افزار CORS را فعال می‌کند، باید پیش‌از میان‌افزار Mvc تعریف شود تا بتواند هدرها را اعمال کند. همچنین نامی که در اینجا به عنوان پارامتر آن استفاده شده، دقیقا به همان نامی که برای تنظیمات سیاست دسترسی درنظر گرفته شد، اشاره می‌کند.
متد app.UseCors، یک متد سراسری است و کل سیستم را داخل این سیاست دسترسی CORS قرار می‌دهد. اگر می‌خواهید صرفا به کنترلر خاصی این تنظیمات اعمال شوند، می‌توان از فیلتر EnableCors با ذکر نام سیاست دسترسی تعریف شده، استفاده کرد (در این حالت ذکر services.AddCors ضروری است و از ذکر app.UseCors صرفنظر می‌شود):
[EnableCors("CorsPolicy")]
public class MyApiController : Controller

و یا اگر می‌خواهید از app.UseCors سراسری استفاده کنید، اما آن‌را برای کنترلر و یا حتی اکشن متد خاصی غیرفعال نمائید، می‌توان از فیلتر [DisableCors] استفاده کرد.

چند نکته:
- به نام رشته‌ای سیاست دسترسی تعریف شده، دقت داشته باشید. اگر این نام را درست ذکر نکنید، هدری تولید نخواهد شد.
- میان‌افزار CORS، برای درخواست‌های same-origin نیز هدری را تولید نمی‌کند.


امکان تعریف سیاست‌های دسترسی بدون نام نیز وجود دارد

در هر دو روش فوق که یکی بر اساس app.UseCors سراسری و دیگری بر اساس فیلتر EnableCors اختصاصی کار می‌کنند، ذکر نام سیاست دسترسی options.AddPolicy ضروری است. اگر علاقه‌ای به ذکر این نام ندارید، می‌توان به طور کامل از تنظیم services.AddCors صرفنظر کرد (قسمت پارامتر و تنظیمات آن‌را ذکر نکرد) و متد app.UseCors را به صورت زیر تنظیم نمود که قابلیت داشتن تنظیمات خاص خود را نیز دارا است:
public void ConfigureServices(IServiceCollection services)
{
      services.AddCors();
      services.AddMvc();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
 
    app.UseCors(policyBuilder =>
    {
        string[] origins = new string[] { "http://localhost:2000", "http://localhost:2001" };
 
        policyBuilder
            .AllowAnyHeader()
            .AllowAnyMethod()
            .WithOrigins(origins);
    });
 
    app.UseMvc();
}

روش دیگر انجام اینکار، تعریف یک DefaultPolicy است:
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddDefaultPolicy(
            builder =>
            {
            builder
                .AllowAnyOrigin()
                .AllowAnyMethod()
                .AllowAnyHeader();
            });
          options.AddPolicy("MyCORSPolicy",
            builder =>
            {
                builder.WithOrigins("http://localhost:49373")
                                    .AllowAnyHeader()
                                    .AllowAnyMethod();
            });
     });
    services.AddMvc().AddNewtonsoftJson();
}
در اینجا در متد AddCors می‌توان چندین AddPolicy نامدار را داشت، به همراه یک AddDefaultPolicy بدون نام. استفاده‌ی از سیاست نام‌دار تعریف شده، یا توسط متد سراسری app.UseCors("MyCORSPolicy") انجام می‌شود و یا توسط فیلتر [EnableCors("MyCORSPolicy")]. اما اگر فیلتر [EnableCors] را بدون ذکر نامی قید کنیم، از همان تنظیمات AddDefaultPolicy استفاده خواهد کرد.


خطاهای متداول حین کار با CORS

خطای کار با SignalR و ارسال اطلاعات اعتبارسنجی کاربر

Access to XMLHttpRequest at 'http://localhost:22135/chat/negotiate?jwt=<removed the jwt key>' 
from origin 'http://localhost:53150' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. 
The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
برای رفع این مشکل کافی است متد AllowCredentials نیز قید شود:
services.AddCors(options =>
{
     options.AddPolicy("AllowAllOrigins",
         builder =>
          {
              builder
                 .AllowAnyOrigin()
                 .AllowAnyHeader()
                 .AllowAnyMethod()
                 .AllowCredentials();
          });
    });

تمام اعتبارسنجی‌ها و اطلاعات کوکی‌ها در سمت سرور قابل دسترسی نیستند

CORS به صورت پیش‌فرض اطلاعات کوکی‌ها را به دومینی دیگر ارسال نمی‌کند. در این حالت اگر حتما نیاز به انجام اینکار است، باید در درخواست Ajax ای ارسالی، خاصیت withCredentials به true تنظیم شود. همچنین سمت سرور نیز هدر Access-Control-Allow-Credentials را تنظیم کند (همان متد AllowCredentials فوق). در اینجا Credentials به کوکی‌ها، هدرهای اعتبارسنجی (مانند هدرهای JWT) کاربران و یا مجوزهای TLS کلاینت‌ها اشاره می‌کند.
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://www.example.com/api/test');
xhr.withCredentials = true;

//In jQuery:
$.ajax({
  type: 'get',
  url: 'http://www.example.com/home',
  xhrFields: {
    withCredentials: true
}

//ES6 fetch API:
fetch(
   'http://remote-url.com/users',
   {
     'PUT',
     headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json'
     },
     mode: 'cors',
     credentials: 'include',
     body: JSON.stringify(body)
   }
)
.then((response) => response.json())
.then((response) => console.log(response))
.catch((error) => console.error(error));

CORS حساس به حروف کوچک و بزرگ است

روش دیگر تنظیم فیلتر EnableCors را در اینجا مشاهده می‌کنید:
[EnableCors(origins: "http://MyWebsite.com", headers: "*", methods: "*")]
//is not the same as this:
[EnableCors(origins:"http://mywebsite.com", headers: "*", methods: "*")]
اگر بجای * دقیقا origins را مشخص کردید، بخاطر داشته باشید که این تنظیم به کوچکی و بزرگی حروف حساس است و در صورت اشتباهی (مانند ذکر MyWebSite بجای mywebsite متداول با حروف کوچک)، درخواست‌های رسیده مسدود خواهند شد. همچنین همانطور که ذکر شد، بین HTTP و HTTPS نیز تفاوت وجود دارد و origin این‌ها یکی درنظر گرفته نمی‌شود و حتی اگر تمام حروف را هم کوچک وارد کرده باشید، باز هم تنظیمات جداگانه‌ای را نیاز خواهند داشت.


در حالت بروز خطای مدیریت نشده‌ای در سمت سرور، ASP.NET Core هدرهای CORS را ارسال نمی‌کند
اطلاعات بیشتر 
نظرات مطالب
استفاده از MVVM زمانیکه امکان Binding وجود ندارد
یک نکته‌ی تکمیلی: Binding موارد پیش بینی نشده در الگوی MVVM

 اگر در پروژه‌ی  xamarin forms، با استفاده از الگوی mvvm خواستید پارامتری را به المانهای کنترلی در صفحه مقید کنید که امکان آن از قبل  فراهم نشده است، می‌توان با استفاده از سفارشی سازی کلاس کنترل، این مشکل را مرتفع کرد. در مثال زیر سعی شده پارامتر Link را در ViewModel که بصورت رشته است، به کنترل WebView  مقید کرد. البته باید در نظر داشت که کنترلر از BindableObject ارث بری کرده باشد تا بتوان از متدهای مرتبط با BindableProperty در کلاس جدید استفاده کرد. نام متغیر ساخته شده از کلاس BindableProperty، دقیقا باید ترکیبی از نام شیء به اضافه Property در آخر باشد که به آن مقید شده است. مثلا برای Uri باید از نام UriProperty استفاده کرد. چون در زمان مقید کردن Uri={Binding Link}  کامپایلر به متغیر UriProperty  نگاشت می‌کند و در زمان تخصیص مستقیم تنها شیء Uri در کلاس MyWebView مقدار دهی می‌شود.
public class MyWebView : WebView
    {
        public static readonly BindableProperty UriProperty =
       BindableProperty.Create(nameof(Uri), typeof(string), typeof(MyWebView), null, propertyChanged: OnItemSourceChanged);

        public string Uri
        {
            get { return (string)GetValue(UriProperty); }
            set { SetValue(UriProperty, value); }
        }

        public static void OnItemSourceChanged(BindableObject bindable, object oldValue, object newValue)
        {
            ((MyWebView)bindable).OnItemSourceChanged((string)oldValue, (string)newValue);
        }
        public void OnItemSourceChanged(string oldValue, string newValue)
        {
           Source = newValue; 
        }     
    }

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage 
...
xmlns:tools="clr-namespace:..."
> <ContentPage.Content> .... <tools:MyWebView Uri="{Binding Link}" /> .... </ContentPage.Content> </ContentPage>
مطالب
EF Code First #10

حین کار با ORMهای پیشرفته، ویژگی‌های جالب توجهی در اختیار برنامه نویس‌ها قرار می‌گیرد که در زمان استفاده از کلاس‌های متداول SQLHelper از آن‌ها خبری نیست؛ مانند:
الف) Deferred execution
ب) Lazy loading
ج) Eager loading

نحوه بررسی SQL نهایی تولیدی توسط EF

برای توضیح موارد فوق، نیاز به مشاهده خروجی SQL نهایی حاصل از ORM است و همچنین شمارش تعداد بار رفت و برگشت به بانک اطلاعاتی. بهترین ابزاری را که برای این منظور می‌توان پیشنهاد داد، برنامه EF Profiler است. برای دریافت آن می‌توانید به این آدرس مراجعه کنید: (^) و (^)

پس از وارد کردن نام و آدرس ایمیل، یک مجوز یک ماهه آزمایشی، به آدرس ایمیل شما ارسال خواهد شد.
زمانیکه این فایل را در ابتدای اجرای برنامه به آن معرفی می‌کنید، محل ذخیره سازی نهایی آن جهت بازبینی بعدی، مسیر MyUserName\Local Settings\Application Data\EntityFramework Profiler خواهد بود.

استفاده از این برنامه هم بسیار ساده است:
الف) در برنامه خود، ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.dll که در پوشه برنامه EFProf موجود است، اضافه کنید.
ب) در نقطه آغاز برنامه، متد زیر را فراخوانی نمائید:
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

نقطه آغاز برنامه می‌تواند متد Application_Start برنامه‌های وب، در متد Program.Main برنامه‌های ویندوزی کنسول و WinForms و در سازنده کلاس App برنامه‌های WPF باشد.
ج) برنامه EFProf را اجرا کنید.

مزایای استفاده از این برنامه
1) وابسته به بانک اطلاعاتی مورد استفاده نیست. (برخلاف برای مثال برنامه معروف SQL Server Profiler که فقط به همراه SQL Server ارائه می‌شود)
2) خروجی SQL نمایش داده شده را فرمت کرده و به همراه Syntax highlighting نیز هست.
3) کار این برنامه صرفا به لاگ کردن SQL تولیدی خلاصه نمی‌شود. یک سری از Best practices را نیز به شما گوشزد می‌کند. بنابراین اگر نیاز دارید سیستم خود را بر اساس دیدگاه یک متخصص بررسی کنید (یک Code review ارزشمند)، این ابزار می‌تواند بسیار مفید باشد.
4) می‌تواند کوئری‌های سنگین و سبک را به خوبی تشخیص داده و گزارشات آماری جالبی را به شما ارائه دهد.
5) می‌تواند دقیقا مشخص کند، کوئری را که مشاهده می‌کنید از طریق کدام متد در کدام کلاس صادر شده است و دقیقا از چه سطری.
6) امکان گروه بندی خودکار کوئری‌های صادر شده را بر اساس DbContext مورد استفاده به همراه دارد.
و ...

استفاده از این برنامه حین کار با EF «الزامی» است! (البته نسخه‌های NH و سایر ORMهای دیگر آن نیز موجود است و این مباحث در مورد تمام ORMهای پیشرفته صادق است)
مدام باید بررسی کرد که صفحه جاری چه تعداد کوئری را به بانک اطلاعاتی ارسال کرده و به چه نحوی. همچنین آیا می‌توان با اعمال اصلاحاتی، این وضع را بهبود بخشید. بنابراین عدم استفاده از این برنامه حین کار با ORMs، همانند راه رفتن در خواب است! ممکن است تصور کنید برنامه دارد به خوبی کار می‌کند اما ... در پشت صحنه فقط صفحه جاری برنامه، 100 کوئری را به بانک اطلاعاتی ارسال کرده، در حالیکه شما تنها نیاز به یک کوئری داشته‌اید.


کلاس‌های مدل مثال جاری

کلاس‌های مدل مثال جاری از یک دپارتمان که دارای تعدادی کارمند می‌باشد، تشکیل شده است. ضمنا هر کارمند تنها در یک دپارتمان می‌تواند مشغول به کار باشد و رابطه many-to-many نیست :

using System.Collections.Generic;

namespace EF_Sample06.Models
{
public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }

//Creates Employee navigation property for Lazy Loading (1:many)
public virtual ICollection<Employee> Employees { get; set; }
}
}

namespace EF_Sample06.Models
{
public class Employee
{
public int EmployeeId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }

//Creates Department navigation property for Lazy Loading
public virtual Department Department { get; set; }
}
}

نگاشت دستی این کلاس‌ها هم ضرورتی ندارد، زیرا قراردادهای توکار EF Code first را رعایت کرده و EF در اینجا به سادگی می‌تواند primary key و روابط one-to-many را بر اساس navigation properties تعریف شده، تشخیص دهد.

در اینجا کلاس Context برنامه به شرح زیر است:

using System.Data.Entity;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
public class Sample06Context : DbContext
{
public DbSet<Department> Departments { set; get; }
public DbSet<Employee> Employees { set; get; }
}
}


و تنظیمات ابتدایی نحوه به روز رسانی و آغاز بانک اطلاعاتی نیز مطابق کدهای زیر می‌باشد:

using System.Collections.Generic;
using System.Data.Entity.Migrations;
using EF_Sample06.Models;

namespace EF_Sample06.DataLayer
{
public class Configuration : DbMigrationsConfiguration<Sample06Context>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}

protected override void Seed(Sample06Context context)
{
var employee1 = new Employee { FirstName = "f name1", LastName = "l name1" };
var employee2 = new Employee { FirstName = "f name2", LastName = "l name2" };
var employee3 = new Employee { FirstName = "f name3", LastName = "l name3" };
var employee4 = new Employee { FirstName = "f name4", LastName = "l name4" };

var dept1 = new Department { Name = "dept 1", Employees = new List<Employee> { employee1, employee2 } };
var dept2 = new Department { Name = "dept 2", Employees = new List<Employee> { employee3 } };
var dept3 = new Department { Name = "dept 3", Employees = new List<Employee> { employee4 } };

context.Departments.Add(dept1);
context.Departments.Add(dept2);
context.Departments.Add(dept3);
base.Seed(context);
}
}
}

نکته: تهیه خروجی XML از نگاشت‌های خودکار تهیه شده

اگر علاقمند باشید که پشت صحنه نگاشت‌های خودکار EF Code first را در یک فایل XML جهت بررسی بیشتر ذخیره کنید، می‌توان از متد کمکی زیر استفاده کرد:

void ExportMappings(DbContext context, string edmxFile)
{
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter writer = XmlWriter.Create(edmxFile, settings))
{
System.Data.Entity.Infrastructure.EdmxWriter.WriteEdmx(context, writer);
}
}

بهتر است پسوند فایل XML تولیدی را edmx قید کنید تا بتوان آن‌را با دوبار کلیک بر روی فایل، در ویژوال استودیو نیز مشاهده کرد:

using (var db = new Sample06Context())
{
ExportMappings(db, "mappings.edmx");
}



الف) بررسی Deferred execution یا بارگذاری به تاخیر افتاده

برای توضیح مفهوم Deferred loading/execution بهترین مثالی را که می‌توان ارائه داد، صفحات جستجوی ترکیبی در برنامه‌ها است. برای مثال یک صفحه جستجو را طراحی کرده‌اید که حاوی دو تکست باکس دریافت FirstName و LastName کاربر است. کنار هر کدام از این تکست باکس‌ها نیز یک چک‌باکس قرار دارد. به عبارتی کاربر می‌تواند جستجویی ترکیبی را در اینجا انجام دهد. نحوه پیاده سازی صحیح این نوع مثال‌ها در EF Code first به چه نحوی است؟

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample06.DataLayer;
using EF_Sample06.Models;

namespace EF_Sample06
{
class Program
{
static IList<Employee> FindEmployees(string fName, string lName, bool byName, bool byLName)
{
using (var db = new Sample06Context())
{
IQueryable<Employee> query = db.Employees.AsQueryable();

if (byLName)
{
query = query.Where(x => x.LastName == lName);
}

if (byName)
{
query = query.Where(x => x.FirstName == fName);
}

return query.ToList();
}
}

static void Main(string[] args)
{
// note: remove this line if you received : create database is not supported by this provider.
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();

Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample06Context, Configuration>());

var list = FindEmployees("f name1", "l name1", true, true);
foreach (var item in list)
{
Console.WriteLine(item.FirstName);
}
}
}
}

نحوه صحیح این نوع پیاده سازی ترکیبی را در متد FindEmployees مشاهده می‌کنید. نکته مهم آن، استفاده از نوع IQueryable و متد AsQueryable است و امکان ترکیب کوئری‌ها با هم.
به نظر شما با فراخوانی متد FindEmployees به نحو زیر که هر دو شرط آن توسط کاربر انتخاب شده است، چه تعداد کوئری به بانک اطلاعاتی ارسال می‌شود؟

var list = FindEmployees("f name1", "l name1", true, true);

شاید پاسخ دهید که سه بار : یکبار در متد db.Employees.AsQueryable و دوبار هم در حین ورود به بدنه شرط‌های یاد شده و اینجا است که کسانی که قبلا با رویه‌های ذخیره شده کار کرده باشند، شروع به فریاد و فغان می‌کنند که ما قبلا این مسایل رو با یک SP در یک رفت و برگشت مدیریت می‌کردیم!
پاسخ صحیح: «فقط یکبار»! آن‌هم تنها در زمان فراخوانی متد ToList و نه قبل از آن.
برای اثبات این مدعا نیاز است به خروجی SQL لاگ شده توسط EF Profiler مراجعه کرد:

SELECT [Extent1].[EmployeeId]              AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[LastName] = 'l name1' /* @p__linq__0 */)
AND ([Extent1].[FirstName] = 'f name1' /* @p__linq__1 */)


IQueryable قلب LINQ است و تنها بیانگر یک عبارت (expression) از رکوردهایی می‌باشد که مد نظر شما است و نه بیشتر. برای مثال زمانیکه یک IQueryable را همانند مثال فوق فیلتر می‌کنید، هنوز چیزی از بانک اطلاعاتی یا منبع داده‌ای دریافت نشده است. هنوز هیچ اتفاقی رخ نداده است و هنوز رفت و برگشتی به منبع داده‌ای صورت نگرفته است. به آن باید به شکل یک expression builder نگاه کرد و نه لیستی از اشیاء فیلتر شده‌ی ما. به این مفهوم، deferred execution (اجرای به تاخیر افتاده) نیز گفته می‌شود.
کوئری LINQ شما تنها زمانی بر روی بانک اطلاعاتی اجرا می‌شود که کاری بر روی آن صورت گیرد مانند فراخوانی متد ToList، فراخوانی متد First یا FirstOrDefault و امثال آن. تا پیش از این فقط به شکل یک عبارت در برنامه وجود دارد و نه بیشتر.
اطلاعات بیشتر: «تفاوت بین IQueryable و IEnumerable در حین کار با ORMs»



ب) بررسی Lazy Loading یا واکشی در صورت نیاز

در مطلب جاری اگر به کلاس‌های مدل برنامه دقت کنید، تعدادی از خواص به صورت virtual تعریف شده‌اند. چرا؟
تعریف یک خاصیت به صورت virtual، پایه و اساس lazy loading است و به کمک آن، تا به اطلاعات شیءایی نیاز نباشد، وهله سازی نخواهد شد. به این ترتیب می‌توان به کارآیی بیشتری در حین کار با ORMs رسید. برای مثال در کلاس‌های فوق، اگر تنها نیاز به دریافت نام یک دپارتمان هست، نباید حین وهله سازی از شیء دپارتمان، شیء لیست کارمندان مرتبط با آن نیز وهله سازی شده و از بانک اطلاعاتی دریافت شوند. به این وهله سازی با تاخیر، lazy loading گفته می‌شود.
Lazy loading پیاده سازی ساده‌ای نداشته و مبتنی است بر بکارگیری AOP frameworks یا کتابخانه‌هایی که امکان تشکیل اشیاء Proxy پویا را در پشت صحنه فراهم می‌کنند. علت virtual تعریف کردن خواص رابط نیز به همین مساله بر می‌گردد، تا این نوع کتابخانه‌ها بتوانند در نحوه تعریف اینگونه خواص virtual در زمان اجرا، در پشت صحنه دخل و تصرف کنند. البته حین استفاده از EF یا انواع و اقسام ORMs دیگر با این نوع پیچیدگی‌ها روبرو نخواهیم شد و تشکیل اشیاء Proxy در پشت صحنه انجام می‌شوند.

یک مثال: قصد داریم اولین دپارتمان ثبت شده در حین آغاز برنامه را یافته و سپس لیست کارمندان آن‌را نمایش دهیم:

using (var db = new Sample06Context())
{
var dept1 = db.Departments.Find(1);
if (dept1 != null)
{
Console.WriteLine(dept1.Name);
foreach (var item in dept1.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}



رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص می‌سازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).

در مثال فوق به صورت خودکار دو کوئری به بانک اطلاعاتی ارسال می‌گردد:

SELECT [Limit1].[DepartmentId] AS [DepartmentId],
[Limit1].[Name] AS [Name]
FROM (SELECT TOP (2) [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name]
FROM [dbo].[Departments] AS [Extent1]
WHERE [Extent1].[DepartmentId] = 1 /* @p0 */) AS [Limit1]


SELECT [Extent1].[EmployeeId] AS [EmployeeId],
[Extent1].[FirstName] AS [FirstName],
[Extent1].[LastName] AS [LastName],
[Extent1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM [dbo].[Employees] AS [Extent1]
WHERE ([Extent1].[Department_DepartmentId] IS NOT NULL)
AND ([Extent1].[Department_DepartmentId] = 1 /* @EntityKeyValue1 */)

یکبار زمانیکه قرار است اطلاعات دپارتمان‌ یک (db.Departments.Find) دریافت شود. تا این لحظه خبری از جدول Employees نیست. چون lazy loading فعال است و فقط اطلاعاتی را که نیاز داشته‌ایم فراهم کرده است.
زمانیکه برنامه به حلقه می‌رسد، نیاز است اطلاعات dept1.Employees را دریافت کند. در اینجا است که کوئری دوم، به بانک اطلاعاتی صادر خواهد شد (بارگذاری در صورت نیاز).


ج) بررسی Eager Loading یا واکشی حریصانه

حالت lazy loading بسیار جذاب به نظر می‌رسد؛ برای مثال می‌توان خواص حجیم یک جدول را به جدول مرتبط دیگری منتقل کرد. مثلا فیلد‌های متنی طولانی یا اطلاعات باینری فایل‌های ذخیره شده، تصاویر و امثال آن. به این ترتیب تا زمانیکه نیازی به اینگونه اطلاعات نباشد، lazy loading از بارگذاری آن‌ها جلوگیری کرده و سبب افزایش کارآیی برنامه می‌شود.
اما ... همین lazy loading در صورت استفاده نا آگاهانه می‌تواند سرور بانک اطلاعاتی را در یک برنامه چندکاربره از پا درآورد! نیازی هم نیست تا شخصی به سایت شما حمله کند. مهاجم اصلی همان برنامه نویس کم اطلاع است!
اینبار مثال زیر را درنظر بگیرید که بجای دریافت اطلاعات یک شخص، مثلا قصد داریم، اطلاعات کلیه دپارتمان‌ها را توسط یک Grid نمایش دهیم (فرقی نمی‌کند برنامه وب یا ویندوز باشد؛ اصول یکی است):

using (var db = new Sample06Context())
{
foreach (var dept in db.Departments)
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}
یک نکته: اگر سعی کنیم کد فوق را اجرا کنیم به خطای زیر برخواهیم خورد:

There is already an open DataReader associated with this Command which must be closed first

برای رفع این مشکل نیاز است گزینه MultipleActiveResultSets=True را به کانکشن استرینگ اضافه کرد:

<connectionStrings>
<clear/>
<add
name="Sample06Context"
connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true;MultipleActiveResultSets=True;"
providerName="System.Data.SqlClient"
/>
</connectionStrings>

سؤال: به نظر شما در دو حلقه تو در توی فوق چندبار رفت و برگشت به بانک اطلاعاتی صورت می‌گیرد؟ با توجه به اینکه در متد Seed ذکر شده در ابتدای مطلب، تعداد رکوردها مشخص است.
پاسخ: 7 بار!


و اینجا است که عنوان شد استفاده از EF Profiler در حین توسعه برنامه‌های مبتنی بر ORM «الزامی» است! اگر از این نکته اطلاعی نداشتید، بهتر است یکبار تمام صفحات گزارش‌گیری برنامه‌های خود را که حاوی یک Grid هستند، توسط EF Profiler بررسی کنید. اگر در این برنامه پیغام خطای n+1 select را دریافت کردید، یعنی در حال استفاده ناصحیح از امکانات lazy loading می‌باشید.

آیا می‌توان این وضعیت را بهبود بخشید؟ زمانیکه کار ما گزارشگیری از اطلاعات با تعداد رکوردهای بالا است، استفاده ناصحیح از ویژگی Lazy loading می‌تواند به شدت کارآیی بانک اطلاعاتی را پایین بیاورد. برای حل این مساله در زمان‌های قدیم (!) بین جداول join می‌نوشتند؛ الان چطور؟
در EF متدی به نام Include جهت Eager loading اطلاعات موجودیت‌های مرتبط به هم درنظر گرفته شده است که در پشت صحنه همینکار را انجام می‌دهد:

using (var db = new Sample06Context())
{
foreach (var dept in db.Departments.Include(x => x.Employees))
{
Console.WriteLine(dept.Name);
foreach (var item in dept.Employees)
{
Console.WriteLine(item.FirstName);
}
}
}

همانطور که ملاحظه می‌کنید اینبار به کمک متد Include، نسبت به واکشی حریصانه Employees اقدام کرده‌ایم. اکنون اگر برنامه را اجرا کنیم، فقط یک رفت و برگشت به بانک اطلاعاتی انجام خواهد شد و کار Join نویسی به صورت خودکار توسط EF مدیریت می‌گردد:

SELECT [Project1].[DepartmentId]            AS [DepartmentId],
[Project1].[Name] AS [Name],
[Project1].[C1] AS [C1],
[Project1].[EmployeeId] AS [EmployeeId],
[Project1].[FirstName] AS [FirstName],
[Project1].[LastName] AS [LastName],
[Project1].[Department_DepartmentId] AS [Department_DepartmentId]
FROM (SELECT [Extent1].[DepartmentId] AS [DepartmentId],
[Extent1].[Name] AS [Name],
[Extent2].[EmployeeId] AS [EmployeeId],
[Extent2].[FirstName] AS [FirstName],
[Extent2].[LastName] AS [LastName],
[Extent2].[Department_DepartmentId] AS [Department_DepartmentId],
CASE
WHEN ([Extent2].[EmployeeId] IS NULL) THEN CAST(NULL AS int)
ELSE 1
END AS [C1]
FROM [dbo].[Departments] AS [Extent1]
LEFT OUTER JOIN [dbo].[Employees] AS [Extent2]
ON [Extent1].[DepartmentId] = [Extent2].[Department_DepartmentId]) AS [Project1]
ORDER BY [Project1].[DepartmentId] ASC,
[Project1].[C1] ASC


متد Include در نگارش‌های اخیر EF پیشرفت کرده است و همانند مثال فوق، امکان کار با lambda expressions را جهت تعریف خواص مورد نظر به صورت strongly typed ارائه می‌دهد. در نگارش‌های قبلی این متد، تنها امکان استفاده از رشته‌ها برای معرفی خواص وجود داشت.
همچنین توسط متد Include امکان eager loading چندین سطح با هم نیز وجود دارد؛ مثلا x.Employees.Kids و همانند آن.


چند نکته در مورد نحوه خاموش کردن Lazy loading

امکان خاموش کردن Lazy loading در تمام کلاس‌های برنامه با تنظیم خاصیت Configuration.LazyLoadingEnabled کلاس Context برنامه به نحو زیر میسر است:

public class Sample06Context : DbContext
{
public Sample06Context()
{
this.Configuration.LazyLoadingEnabled = false;
}

یا اگر تنها در مورد یک کلاس نیاز است این خاموش سازی صورت گیرد، کلمه کلیدی virtual را حذف کنید. برای مثال با نوشتن public ICollection<Employee> Employees بجای public virtual ICollection<Employee> Employees در اولین بار وهله سازی کلاس دپارتمان، لیست کارمندان آن به نال تنظیم می‌شود. البته در این حالت null object pattern را نیز فراموش نکنید (وهله سازی پیش فرض Employees در سازنده کلاس):

public class Department
{
public int DepartmentId { get; set; }
public string Name { get; set; }

public ICollection<Employee> Employees { get; set; }
public Department()
{
Employees = new HashSet<Employee>();
}
}

به این ترتیب به خطای null reference object بر نخواهیم خورد. همچنین وهله سازی، با مقدار دهی لیست دریافتی از بانک اطلاعاتی متفاوت است. در اینجا نیز باید از متد Include استفاده کرد.

بنابراین در صورت خاموش کردن lazy loading، حتما نیاز است از متد Include استفاده شود. اگرlazy loading فعال است، جهت تبدیل آن به eager loading از متد Include استفاده کنید (اما اجباری نیست).
بازخوردهای پروژه‌ها
آموزش WPF
سلام و خسته نباشید با توجه به توسعه نرم افزارهای تحت ویندوز توسط WPF از شما خواهشمندم مطالبی را در زمینه آموزش آن به سایت اضافه نمایید. با تشکر
مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت یازدهم
در این قسمت قصد بررسی کامپوننت‌های فوق العاده‌ی Syncfusion را داریم. احتمالا Syncfusion را با کتاب‌های Succinctly Series آن می شناسید. این شرکت برای Xamarin Forms نزدیک به 130‌ کامپوننت، شامل موارد کار با دیتا، اعم از فرم‌های Data Entry ،Data Grid و ListView را نوشته که در کنار کنترل‌های کار با فایل‌های Office-PDF و همچنین گزارشات و چارت‌ها و سایر کنترل‌های آن، نیاز هر برنامه‌ای را برآورده می‌کند. یکی از چند ده کتاب Xamarin Forms نیز توسط این شرکت نوشته شده‌است.
ما ضمن استفاده کامپوننت List View آن، هم کار با List View را یاد می‌گیریم و هم کار با Syncfusion را. در نظر داشته باشید که خود Xamarin Forms نیز List View دارد و در نسخه‌ی 4 آن که هم اکنون در مرحله‌ی Preview است، کنترل جدیدی به نام CollectionView نیز ارائه شده که امکانات خیلی خوبی دارد.
توجه: مطالب آموزش زیر، از این لینک آورده شده‌است.

برای شروع، ابتدا Nuget Package مربوطه را در پروژه‌ی XamApp نصب کنید. نیازی به نصب کردن آن در XamApp.Android و XamApp.iOS و XamApp.UWP نیست، ولی باز و بسته کردن ویژوال استودیو بعد از نصب و راست کلیک کردن بر روی Solution و زدن Restore nuget packages ایده‌ی خوبی است!

در پروژه‌ی iOS، در فایل AppDelegate.cs، بعد از Forms.Init، کد زیر را کپی کنید:

SfListViewRenderer.Init();

همین کد را در MainPage.xaml.cs در پروژه UWP، قبل از LoadApplication قرار دهید. نیازی به انجام کاری در Android نیست.

سپس Product Key این محصول را به دست آورده و در پروژه XamApp، فولدر Views در فایل SyncfusionLicense قرار دهید.

حال برای نمایش لیستی از محصولات، ابتدا کلاس Product را ایجاد می‌کنیم. چه در زمانیکه یک Rest api را در سمت سرور فراخوانی می‌کنیم و چه زمانیکه با دیتابیس بر روی گوشی یعنی Sqlite کار می‌کنیم، در نهایت لیستی از یک کلاس را داریم (در اینجا Product).

    public class Product : Bindable
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsActive { get; set; }
        public decimal Price { get; set; }
    }

در یک View Model جدید با نام ProductsViewModel، در OnNavigatedToAsync، دیتا را از سرور یا دیتابیس، بر روی گوشی دریافت می‌کنیم؛ اما در این مثال، برای راحتی بیشتر یک List را New می‌کنیم:

    public class ProductsViewModel : BitViewModelBase
    {
        public List<Product> Products { get; set; }

        public async override Task OnNavigatedToAsync(INavigationParameters parameters)
        {
            Products = new List<Product> // getting products from server or sqlite database
            {
                new Product { Id = 1, IsActive = true, Name = "Product1" , Price = 12.2m /* m => decimal */ },
                new Product { Id = 2, IsActive = false, Name = "Product2" , Price = 14 },
                new Product { Id = 3, IsActive = true, Name = "Product3" , Price = 11 },
            };
            await base.OnNavigatedToAsync(parameters);
        }
    }

حال نوبت به دادن یک Template می‌رسد. مثلا فرض کنید می‌خواهیم نام را درون یک Label نمایش دهیم و بر اساس فعال یا غیر فعال بودن Product، یک Checkbox را تغییر داده، تیک بزنیم یا نزنیم و در نهایت نمایش قیمت را در یک Label دیگر خواهیم داشت.

    <sfListView:SfListView ItemsSource="{Binding Products}">
        <sfListView:SfListView.ItemTemplate>
            <DataTemplate>
                <FlexLayout x:DataType="model:Product" Direction="Row">
                    <Label
                        FlexLayout.Basis="50%"
                        Text="{Binding Name}"
                        VerticalTextAlignment="Center" />
                    <bitControls:BitCheckbox InputTransparent="True" FlexLayout.Basis="25%" IsChecked="{Binding IsActive}" />
                    <Label
                        FlexLayout.Basis="25%"
                        Text="{Binding Price}"
                        VerticalTextAlignment="Center" />
                </FlexLayout>
            </DataTemplate>
        </sfListView:SfListView.ItemTemplate>
    </sfListView:SfListView>

همانطور که می‌بینید، در DataTemplate از Flex Layout استفاده شده است. Flex Layout در کنار Grid, Stack, Relative, Absolute و سایر Layout‌‌های Xamarin Forms در پروژه قابلیت استفاده دارد و مزیت‌های خاص خود را دارد.

این Data Template توسط List View، حداکثر سه بار ساخته می‌شود؛ چون View Model در لیست مثال خود، سه Product دارد. خود List View تکنیک‌های Virtualization و Cell Reuse را بدون نیاز به هیچ کد اضافه‌ای هندل می‌کند و Performance خوبی دارد. در View مربوطه یعنی ProductsView.xaml، هر Binding ای (مثل Binding Products) به View Model اشاره می‌کند، اما درون Data Template، هر Binding به Product ای اشاره می‌کند که آن ردیف List View، دارد نمایش‌اش می‌دهد. برای همین x:DataType را روی Flex Layout درون Data Template به Product وصل کرده‌ایم. در این صورت اگر بنویسیم Binding N_ame، به ما خطا داده می‌شود که کلاس Product هیچ Property با نام N_ame ندارد که خطای درستی است.

روی BitCheckbox مقدار InputTransparent را برابر با True داده‌ایم که باعث می‌شود کلیک روی Checkbox عملا در نظر گرفته نشود. این منطقی است، زیرا عوض کردن مقدار Checkbox در این مثال ما ذخیره نمی‌شود و کاربرد نمایشی دارد و فقط باعث گیج شدن کاربر می‌شود.

کنترل BitCheckbox از مجموعه کنترل‌های Bit است که اخیرا با BitDatePicker آن آشنا شده‌اید. برای آشنایی با نحوه افزودن این کنترل‌ها به یک پروژه، به مستندات Bit Framework مراجعه کنید. خود Syncfusion نیز Checkbox دارد.

حال فرض کنید که قرار است دکمه‌ای برای هر ردیف List View داشته باشیم که با زدن روی آن، اطلاعات Product به سرور ارسال شود و جزئیات بیشتری دریافت و در قالب یک Alert نمایش داده شود.  برای این کار، ابتدا به Data Template که Flex Layout است، یک دکمه اضافه می‌کنیم. سپس Command آن دکمه را به View Model بایند می‌کنیم. در آن Command البته احتیاج داریم بدانیم درخواست نمایش جزئیات بیشتر، برای کدام Product داده شده. این مهم با Command Parameter شدنی است.

برای پیاده سازی این مثال، در سمت View Model داریم:

 
public BitDelegateCommand<Product> ShowProductDetailsCommand { get; set; }
public IUserDialogs UserDialogs { get; set; } async Task ShowProductDetails(Product product) { string productDetail = $"Product: {product.Name}'s more info: ..."; // get more info from server. await UserDialogs.AlertAsync(productDetail, "Product Detail"); }

کامند ShowProductDetailCommand یک پارامتر را از جنس Product می‌گیرد و آن Product ای است که روی دکمه آن کلیک شده‌است. با Clone کردن آخرین نسخه XamApp و درخواست نمایش صفحه‌ی Products در App.xaml.cs به صورت زیر و اجرای برنامه، می‌توانید درک بهتری از عملکرد آن داشته باشید:

await NavigationService.NavigateAsync("/Nav/Products", animated: false);

سپس در View مربوطه داریم:

 
...
<Button Command="{Binding ShowProductDetailsCommand}" CommandParameter="{Binding .}" Text="Detail..." /> </FlexLayout> </DataTemplate>

CommandParameter اگر برابر با Binding Id می‌بود، به Command در سمت View Model، بجای کل Product، فقط Id آن ارسال می‌شد. ولی Show Product Detail Command منتظر یک Product کامل است، نه فقط Id آن. با نوشتن 

CommandParameter="{Binding .}"

کل Product با کلیک روی دکمه به Command ارسال می‌شود.

اکنون اگر پروژه را Build کنید، خطایی را از x:DataType خواهید گرفت که منطقی است. اگر Binding Name و Binding Price دو Property با نام‌های Name و Price را از کلاس Product جستجو می‌کنند، پس قاعدتا ShowProductDetailCommand نیز در همان کلاس مدل، یعنی Product جستجو می‌شود! ولی می‌دانیم که این Command در View Model ما یعنی ProductsViewModel است. برای حل این مشکل، به جای Binding از bit:ViewModelBinding استفاده می‌کنیم:

Command="{bit:ViewModelBinding ShowProductDetailsCommand}"

در این صورت، بجای جستجو کردن ShowProductDetailCommand در کلاس Product، این را در ProductsViewModel جستجو می‌کند که منجر به خروجی درست می‌شود.

این List View دارای امکاناتی چون Infinite loading، Pull to refresh و Grouping-Sorting-Filtering و ... است که می‌توانید از روی مستندات خوب Syncfusion، آنها را راه اندازی کنید و اگر به مشکلی برخوردید نیز اینجا بپرسید. همچنین نگاهی به لیست 129 کنترل دیگر بیاندازید و ببینید که در برنامه‌های خود از کدام یک از آنها می‌توانید استفاده کنید.