مطالب
OpenCVSharp #17
تشخیص اشخاص به کمک OpenCV

فرض کنید قصد دارید یک سیستم حضور غیاب مبتنی بر تشخیص چهره را طراحی کنید. قسمت استخراج چهره، از تصویر کلی رسیده را بررسی کردیم. اما در ادامه چگونه تشخیص دهیم که این چهره متعلق به چه شخصی است؟ با توجه به اینکه تصویر چهره‌ی یک شخص می‌تواند از زوایای مختلفی تهیه شود و یا حتی حالات روحی منعکس شده‌ی در صورت نیز در تغییر بیت و بایت‌های تصویر چهره مؤثر هستند.


بانک اطلاعاتی تصاویر چهره‌های اشخاص

در اینجا از تصاویر «The Database of Faces» استفاده خواهیم کرد. این مجموعه شامل تصاویر 40 شخص، در 10 حالت مختلف است.


برای بارگذاری این تصاویر و استفاده‌ی از آن‌ها در الگوریتم FisherFaceRecognizer نیاز به ساختار ذیل است:
public class ImageInfo
{
    public Mat Image { set; get; }
    public int ImageGroupId { set; get; }
    public int ImageId { set; get; }
}
در اینجا Image، محتوای تصویر انتخابی است. مقدار ImageGroupId مساوی مقدار عددی نام پوشه‌ی تصاویر منهای یک، تنظیم می‌شود. برای مثال پوشه‌ی s1 به گروه صفر تنظیم می‌شود. ImageId نیز به یک مقدار خود افزایش یابنده معادل شماره‌ی جاری تصویر، تنظیم می‌گردد؛ به این صورت:
var images = new List<ImageInfo>();
 
var imageId = 0;
foreach (var dir in new DirectoryInfo(@"..\..\Images").GetDirectories())
{
    var groupId = int.Parse(dir.Name.Replace("s", string.Empty)) - 1;
    foreach (var imageFile in dir.GetFiles("*.pgm"))
    {
        images.Add(new ImageInfo
        {
            Image = new Mat(imageFile.FullName, LoadMode.GrayScale),
            ImageId = imageId++,
            ImageGroupId = groupId
        });
    }
}
ابتدا پوشه‌های دیتابیس تصاویر یافت شده و سپس از نام هر پوشه یک شما‌‌ره‌ی گروه (یا شماره‌ی شخص) استخراج می‌شود. سپس تصاویر این پوشه به لیست تصاویر اصلی اضافه خواهند شد.


تشخیص یک چهره‌ی اتفاقی

پس از تشکیل لیست تصاویر، اکنون کار با الگوریتم FisherFaceRecognizer به نحو ذیل خواهد بود:
var model = FaceRecognizer.CreateFisherFaceRecognizer();
model.Train(images.Select(x => x.Image), images.Select(x => x.ImageGroupId));
 
var rnd = new Random();
var randomImageId = rnd.Next(0, images.Count - 1);
var testSample = images[randomImageId];
 
Console.WriteLine("Actual group: {0}", testSample.ImageGroupId);
Cv2.ImShow("actual", testSample.Image);
 
var predictedGroupId = model.Predict(testSample.Image);
Console.WriteLine("Predicted group: {0}", predictedGroupId);
پارامتر اول متد Train، لیست تصاویر است و پارامتر دوم، لیست شماره گروه‌های متناظر با هر تصویر است که در اینجا به عنوان برچسب نیز نامگذاری شده‌است.
سپس با استفاده از کلاس Random، یک تصویر اتفاقی انتخاب می‌شود.
اکنون این تصویر اتفاقی به متد Predict ارسال شده و نتیجه‌ی آن، شماره گروه چهره‌ی تشخیص داده شده‌است. به این ترتیب می‌توان تشخیص داد که یک تصویر مفروض ورودی، متعلق به چه شخصی (یا در اینجا گروه یا برچسب) است.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
مطالب
EF Code First #12

پیاده سازی الگوی Context Per Request در برنامه‌های مبتنی بر EF Code first

در طراحی برنامه‌های چند لایه مبتنی بر EF مرسوم نیست که در هر کلاس و متدی که قرار است از امکانات آن استفاده کند، یکبار DbContext و کلاس مشتق شده از آن وهله سازی شوند؛ به این ترتیب امکان انجام امور مختلف در طی یک تراکنش از بین می‌رود. برای حل این مشکل الگویی مطرح شده است به نام Session/Context Per Request و یا به اشتراک گذاری یک Unit of work در لایه‌های مختلف برنامه در طی یک درخواست، که در ادامه یک پیاده سازی آن‌را با هم مرور خواهیم کرد.
البته این سشن با سشن ASP.NET یکی نیست. در NHibernate معادل DbContextایی که در اینجا ملاحظه می‌کنید، Session نام دارد.


اهمیت بکارگیری الگوی Unit of work و به اشتراک گذاری آن در طی یک درخواست

در الگوی واحد کار یا همان DbContext در اینجا، تمام درخواست‌های رسیده به آن، در صف قرار گرفته و تمام آن‌ها در پایان کار، به بانک اطلاعاتی اعمال می‌شوند. برای مثال زمانیکه شیءایی را به یک وهله از DbContext اضافه/حذف می‌کنیم، یا در ادامه مقدار خاصیتی را تغییر می‌دهیم، هیچکدام از این تغییرات تا زمانیکه متد SaveChanges فراخوانی نشود، به بانک اطلاعاتی اعمال نخواهند شد. این مساله مزایای زیر را به همراه خواهد داشت:

الف) کارآیی بهتر
در اینجا از یک کانکشن باز شده، حداکثر استفاده صورت می‌گیرد. چندین و چند عملیات در طی یک batch به بانک اطلاعاتی اعمال می‌گردند؛ بجای اینکه برای اعمال هرکدام، یکبار اتصال جداگانه‌ای به بانک اطلاعاتی باز شود.

ب) بررسی مسایل همزمانی
استفاده از یک الگوی واحد کار، امکان بررسی خودکار تمام تغییرات انجام شده بر روی یک موجودیت را در متدها و لایه‌های مختلف میسر کرده و به این ترتیب مسایل مرتبط با ConcurrencyMode عنوان شده در قسمت‌های قبل به نحو بهتری قابل مدیریت خواهند بود.

ج) استفاده صحیح از تراکنش‌ها
الگوی واحد کار به صورت خودکار از تراکنش‌ها استفاده می‌کند. اگر در حین فراخوانی متد SaveChanges مشکلی رخ دهد، کل عملیات Rollback خواهد شد و تغییری در بانک اطلاعاتی رخ نخواهد داد. بنابراین استفاده از یک تراکنش در حین چند عملیات ناشی از لایه‌های مختلف برنامه، منطقی‌تر است تا اینکه هر کدام، در تراکنشی جدا مشغول به کار باشند.


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

در مثالی که در این قسمت بررسی خواهیم کرد، از کلاس‌های مدل گروه محصولات کمک گرفته شده است:

using System.Collections.Generic; 
namespace EF_Sample07.DomainClasses { public class Category { public int Id { get; set; } public virtual string Name { get; set; } public virtual string Title { get; set; } public virtual ICollection<Product> Products { get; set; } } }

using System.ComponentModel.DataAnnotations; 
namespace EF_Sample07.DomainClasses { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; }
[ForeignKey("CategoryId")] public virtual Category Category { get; set; } public int CategoryId { get; set; } } }


در کلاس Product، یک خاصیت اضافی به نام CategoryId اضافه شده است که توسط ویژگی ForeignKey، به عنوان کلید خارجی جدول معرفی خواهد شد. از این خاصیت در برنامه‌های ASP.NET برای مقدار دهی یک کلید خارجی توسط یک DropDownList پر شده با لیست گروه‌ها، استفاده خواهیم کرد.



پیاده سازی الگوی واحد کار

همانطور که در قسمت قبل نیز ذکر شد، DbContext در EF Code first بر اساس الگوی واحد کار تهیه شده است، اما برای به اشتراک گذاشتن آن بین لایه‌های مختلف برنامه نیاز است یک لایه انتزاعی را برای آن تهیه کنیم، تا بتوان آن‌را به صورت خودکار توسط کتابخانه‌های Dependency Injection یا به اختصار DI در زمان نیاز به استفاده از آن‌، به کلاس‌های استفاده کننده تزریق کنیم. کتابخانه‌ی DI ایی که در این قسمت مورد استفاده قرار می‌گیرد، کتابخانه معروف StructureMap است. برای دریافت آن می‌توانید از Nuget استفاده کنید؛ یا از صفحه اصلی آن در Github : (^).
اینترفیس پایه الگوی واحد کار ما به شرح زیر است:

using System.Data.Entity;
using System; 
namespace EF_Sample07.DataLayer.Context { public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveChanges(); } }

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

using System.Data.Entity;
using EF_Sample07.DomainClasses; 
namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
#region IUnitOfWork Members public new IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } #endregion } }

توضیحات:
با کلاس Context در قسمت‌های قبل آشنا شده‌ایم. در اینجا به معرفی کلاس‌هایی خواهیم پرداخت که در معرض دید EF Code first قرار خواهند گرفت.
DbSetها هم معرف الگوی Repository هستند. کلاس Sample07Context، معرفی الگوی واحد کار یا Unit of work برنامه است.
برای اینکه بتوانیم تعاریف کلاس‌های سرویس برنامه را مستقل از تعریف کلاس Sample07Context کنیم، یک اینترفیس جدید را به نام IUnitOfWork به برنامه اضافه کرده‌ایم.
در اینجا کلاس Sample07Context پیاده سازی کننده اینترفیس IUnitOfWork خواهد بود (اولین تغییر).
دومین تغییر هم استفاده از متد base.Set می‌باشد. به این ترتیب به سادگی می‌توان به DbSetهای مختلف در حین کار با IUnitOfWork دسترسی پیدا کرد. به عبارتی ضرورتی ندارد به ازای تک تک DbSetها یکبار خاصیت جدیدی را به اینترفیس IUnitOfWork اضافه کرد. به کمک استفاده از امکانات Generics مهیا، اینبار
uow.Set<Product> 

معادل همان db.Products سابق است؛ در حالتیکه از Sample07Context به صورت مستقیم استفاده شود.
همچنین نیازی به پیاده سازی متد SaveChanges نیست؛ زیرا پیاده سازی آن در کلاس DbContext قرار دارد.


استفاده از الگوی واحد کار در کلاس‌های لایه سرویس برنامه

using EF_Sample07.DomainClasses;
using System.Collections.Generic; 
namespace EF_Sample07.ServiceLayer { public interface ICategoryService { void AddNewCategory(Category category); IList<Category> GetAllCategories(); } }

using EF_Sample07.DomainClasses;
using System.Collections.Generic; 
namespace EF_Sample07.ServiceLayer { public interface IProductService { void AddNewProduct(Product product); IList<Product> GetAllProducts(); } }

لایه سرویس برنامه را با دو اینترفیس جدید شروع می‌کنیم. هدف از این اینترفیس‌ها، ارائه پیاده سازی‌های متفاوت، به ازای ORMهای مختلف است. برای مثال در کلاس‌های زیر که نام آن‌ها با Ef شروع شده است، پیاده سازی خاص Ef Code first را تدارک خواهیم دید. این پیاده سازی، قابل انتقال به سایر ORMها نیست چون نه پیاده سازی یکسانی را از مباحث LINQ ارائه می‌دهند و نه متدهای الحاقی همانندی را به همراه دارند و نه اینکه مباحث نگاشت کلاس‌های آن‌ها به جداول مختلف یکی است:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses; 
namespace EF_Sample07.ServiceLayer { public class EfCategoryService : ICategoryService { IUnitOfWork _uow; IDbSet<Category> _categories; public EfCategoryService(IUnitOfWork uow) { _uow = uow; _categories = _uow.Set<Category>(); }
public void AddNewCategory(Category category) { _categories.Add(category); }
public IList<Category> GetAllCategories() { return _categories.ToList(); } } }

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses; 
namespace EF_Sample07.ServiceLayer { public class EfProductService : IProductService { IUnitOfWork _uow; IDbSet<Product> _products; public EfProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); }
public void AddNewProduct(Product product) { _products.Add(product); }
public IList<Product> GetAllProducts() { return _products.Include(x => x.Category).ToList(); } } }


توضیحات:
همانطور که ملاحظه می‌کنید در هیچکدام از کلاس‌های سرویس برنامه، وهله سازی مستقیمی از الگوی واحد کار وجود ندارد. این لایه از برنامه اصلا نمی‌داند که کلاسی به نام Sample07Context وجود خارجی دارد یا خیر.
همچنین لایه اضافی دیگری را به نام Repository جهت مخفی سازی سازوکار EF به برنامه اضافه نکرده‌ایم. این لایه شاید در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نیست و سبب تحمیل سربار اضافی بی موردی به برنامه می‌شود؛ ORMها ویژگی‌های یکسانی را ارائه نمی‌دهند. حتی در حالت استفاده از LINQ، پیاده سازی‌های یکسانی را به همراه ندارند.
بنابراین اگر قرار است برنامه مستقل از ORM کار کند، نیاز است لایه استفاده کننده از سرویس برنامه، با دو اینترفیس IProductService و ICategoryService کار کند و نه به صورت مستقیم با پیاده سازی آن‌ها. به این ترتیب هر زمان که لازم شد، فقط باید پیاده سازی‌های کلاس‌های سرویس را تغییر داد؛ باز هم برنامه نهایی بدون نیاز به تغییری کار خواهد کرد.

تا اینجا به معماری پیچیده‌ای نرسیده‌ایم و اصطلاحا over-engineering صورت نگرفته است. یک اینترفیس بسیار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه این اینترفیس به کلاس‌های سرویس برنامه تزریق شده است (تزریق وابستگی در سازنده کلاس). کلاس‌های سرویس ما «می‌دانند» که EF وجود خارجی دارد و سعی نکرده‌ایم توسط لایه اضافی دیگری آن‌را مخفی کنیم. شیوه کار با IDbSet تعریف شده دقیقا همانند روال متداولی است که با EF Code first کار می‌شود و بسیار طبیعی جلوه می‌کند.


استفاده از الگوی واحد کار و کلاس‌های سرویس تهیه شده در یک برنامه کنسول ویندوزی

در ادامه برای وهله سازی اینترفیس‌های سرویس و واحد کار برنامه، از کتابخانه StructureMap که یاد شد، استفاده خواهیم کرد. بنابراین، تمام برنامه‌های نهایی ارائه شده در این قسمت، ارجاعی را به اسمبلی StructureMap.dll نیاز خواهند داشت.
کدهای برنامه کنسول مثال جاری را در ادامه ملاحظه خواهید کرد:

using System.Collections.Generic;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using StructureMap; 
namespace EF_Sample07 { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>(); x.For<ICategoryService>().Use<EfCategoryService>(); });
var uow = ObjectFactory.GetInstance<IUnitOfWork>(); var categoryService = ObjectFactory.GetInstance<ICategoryService>();
var product1 = new Product { Name = "P100", Price = 100 }; var product2 = new Product { Name = "P200", Price = 200 }; var category1 = new Category { Name = "Cat100", Title = "Title100", Products = new List<Product> { product1, product2 } }; categoryService.AddNewCategory(category1); uow.SaveChanges(); } } }

در اینجا بیشتر هدف، معرفی نحوه استفاده از StructureMap است.
ابتدا توسط متد ObjectFactory.Initialize مشخص می‌کنیم که اگر برنامه نیاز به اینترفیس IUnitOfWork داشت، لطفا کلاس Sample07Context را وهله سازی کرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر باید از کلاس EfCategoryService تامین شود.
توسط ObjectFactory.GetInstance نیز می‌توان به وهله‌ای از این کلاس‌ها دست یافت و نهایتا با فراخوانی uow.SaveChanges می‌توان اطلاعات را ذخیره کرد.

چند نکته:
- به کمک کتابخانه StructureMap، تزریق IUnitOfWork به سازنده کلاس EfCategoryService به صورت خودکار انجام می‌شود. اگر به کدهای فوق دقت کنید ما فقط با اینترفیس‌ها مشغول به کار هستیم، اما وهله‌سازی‌ها در پشت صحنه انجام می‌شود.
- حین معرفی IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. این enum مقادیر زیر را می‌تواند بپذیرد:

public enum InstanceScope
{
        PerRequest = 0,
        Singleton = 1,
        ThreadLocal = 2,
        HttpContext = 3,
        Hybrid = 4,
        HttpSession = 5,
        HybridHttpSession = 6,
        Unique = 7,
        Transient = 8,
} 

برای مثال اگر در برنامه‌ای نیاز داشتید یک کلاس به صورت Singleton عمل کند، فقط کافی است نحوه کش شدن آن‌را تغییر دهید.
حالت PerRequest در برنامه‌های وب کاربرد دارد (و حالت پیش فرض است). با انتخاب آن وهله سازی کلاس مورد نظر به ازای هر درخواست رسیده انجام خواهد شد.
در حالت ThreadLocal، به ازای هر Thread، وهله‌ای متفاوت در اختیار مصرف کننده قرار می‌گیرد.
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی می‌گردد.
حالت Hybrid ترکیبی است از حالت‌های HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ می‌کند.


استفاده از الگوی واحد کار و کلاس‌های سرویس تهیه شده در یک برنامه ASP.NET MVC

یک برنامه خالی ASP.NET MVC را آغاز کنید. سپس یک HomeController جدید را نیز به آن اضافه نمائید و کدهای آن‌را مطابق اطلاعات زیر تغییر دهید:
using System.Web.Mvc;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer;
using EF_Sample07.DataLayer.Context;
using System.Collections.Generic; 
namespace EF_Sample07.MvcAppSample.Controllers { public class HomeController : Controller { IProductService _productService; ICategoryService _categoryService; IUnitOfWork _uow; public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService) { _productService = productService; _categoryService = categoryService; _uow = uow; }
[HttpGet] public ActionResult Index() { var list = _productService.GetAllProducts(); return View(list); }
[HttpGet] public ActionResult Create() { ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name"); return View(); }
[HttpPost] public ActionResult Create(Product product) { if (this.ModelState.IsValid) { _productService.AddNewProduct(product); _uow.SaveChanges(); }
return RedirectToAction("Index"); }
[HttpGet] public ActionResult CreateCategory() { return View(); }
[HttpPost] public ActionResult CreateCategory(Category category) { if (this.ModelState.IsValid) { _categoryService.AddNewCategory(category); _uow.SaveChanges(); }
return RedirectToAction("Index"); } } }

نکته مهم این کنترلر، تزریق وابستگی‌ها در سازنده کلاس کنترلر است؛ به این ترتیب کنترلر جاری نمی‌داند که با کدام پیاده سازی خاصی از این اینترفیس‌ها قرار است کار کند.
اگر برنامه را به همین نحو اجرا کنیم، موتور ASP.NET MVC ایراد خواهد گرفت که یک کنترلر باید دارای سازنده‌ای بدون پارامتر باشد تا من بتوانم به صورت خودکار وهله‌ای از آن‌را ایجاد کنم. برای رفع این مشکل از کتابخانه StructureMap برای تزریق خودکار وابستگی‌ها کمک خواهیم گرفت:

using System;
using System.Data.Entity;
using System.Web.Mvc;
using System.Web.Routing;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap; 
namespace EF_Sample07.MvcAppSample
{ // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
protected void Application_Start() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); initStructureMap(); }
private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>(); });
//Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); }
protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } }

توضیحات:
کدهای فوق متعلق به کلاس Global.asax.cs هستند. در اینجا در متد Application_Start، متد initStructureMap فراخوانی شده است.
با پیاده سازی ObjectFactory.Initialize در کدهای برنامه کنسول معرفی شده آشنا شدیم. اینبار فقط حالت کش شدن کلاس Context برنامه را HttpContextScoped قرار داده‌ایم تا به ازای هر درخواست رسیده یک بار الگوی واحد کار وهله سازی شود.
نکته مهمی که در اینجا اضافه شده‌است، استفاده از متد ControllerBuilder.Current.SetControllerFactory می‌باشد. این متد نیاز به وهله‌ای از نوع DefaultControllerFactory دارد که نمونه‌ای از آن‌را در کلاس StructureMapControllerFactory مشاهده می‌کنید. به این ترتیب در زمان وهله سازی خودکار یک کنترلر، اینبار StructureMap وارد عمل شده و وابستگی‌های برنامه را مطابق تعاریف ObjectFactory.Initialize ذکر شده، به سازنده کلاس کنترلر تزریق می‌کند.
همچنین در متد Application_EndRequest با فراخوانی ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتی اتصالات به بانک اطلاعاتی جلوگیری خواهیم کرد. چون وهله الگوی کار برنامه HttpScoped تعریف شده، در پایان یک درخواست به صورت خودکار توسط StructureMap پاکسازی می‌شود و به نشتی منابع نخواهیم رسید.


استفاده از الگوی واحد کار و کلاس‌های سرویس تهیه شده در یک برنامه ASP.NET Web forms

در یک برنامه ASP.NET Web forms نیز می‌توان این مباحث را پیاده سازی کرد:

using System;
using System.Data.Entity;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap; 
namespace EF_Sample07.WebFormsAppSample { public class Global : System.Web.HttpApplication { private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
x.SetAllProperties(y=> { y.OfType<IUnitOfWork>(); y.OfType<ICategoryService>(); y.OfType<IProductService>(); }); }); }
void Application_Start(object sender, EventArgs e) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); initStructureMap(); }
void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); }

در اینجا کدهای کلاس Global.asax.cs را ملاحظه می‌کنید. توضیحات آن با قسمت ASP.NET MVC آنچنان تفاوتی ندارد و یکی است. البته منهای تعاریف SetAllProperties که جدید است و در ادامه به علت اضافه کردن آن‌ها خواهیم رسید.
در ASP.NET Web forms برخلاف ASP.NET MVC نیاز است کار وهله سازی اینترفیس‌ها را به صورت دستی انجام دهیم. برای این منظور و کاهش کدهای تکراری برنامه می‌توان یک کلاس پایه را به نحو زیر تعریف کرد:

using System.Web.UI;
using StructureMap; 
namespace EF_Sample07.WebFormsAppSample { public class BasePage : Page { public BasePage() { ObjectFactory.BuildUp(this); } } }

سپس برای استفاده از آن خواهیم داشت:

using System;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.DomainClasses;
using EF_Sample07.ServiceLayer; 
namespace EF_Sample07.WebFormsAppSample { public partial class AddProduct : BasePage { public IUnitOfWork UoW { set; get; } public IProductService ProductService { set; get; } public ICategoryService CategoryService { set; get; }
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { bindToCategories(); } }
private void bindToCategories() { ddlCategories.DataTextField = "Name"; ddlCategories.DataValueField = "Id"; ddlCategories.DataSource = CategoryService.GetAllCategories(); ddlCategories.DataBind(); }
protected void btnAdd_Click(object sender, EventArgs e) { var product = new Product { Name = txtName.Text, Price = int.Parse(txtPrice.Text), CategoryId = int.Parse(ddlCategories.SelectedItem.Value) }; ProductService.AddNewProduct(product); UoW.SaveChanges(); Response.Redirect("~/Default.aspx"); } } }


اینبار وابستگی‌های کلاس افزودن محصولات، به صورت خواصی عمومی تعریف شده‌اند. این خواص عمومی توسط متد SetAllProperties که در فایل global.asax.cs معرفی شدند، باید یکبار تعریف شوند (مهم!).
سپس اگر دقت کرده باشید، اینبار کلاس AddProduct از BasePage ما ارث بری کرده است. در سازند کلاس BasePage، با فراخوانی متد ObjectFactory.BuildUp، تزریق وابستگی‌ها به خواص عمومی کلاس جاری صورت می‌گیرد.
در ادامه نحوه استفاده از این اینترفیس‌ها را جهت مقدار دهی یک DropDownList یا ذخیره سازی اطلاعات یک محصول مشاهده می‌کنید. در اینجا نیز کار با اینترفیس‌ها انجام شده و کلاس جاری دقیقا نمی‌داند که با چه وهله‌ای مشغول به کار است. تنها در زمان اجرا است که توسط StructureMap ، به ازای هر اینترفیس معرفی شده، وهله‌ای مناسب بر اساس تعاریف فایل Global.asax.cs در اختیار برنامه قرار می‌گیرد.

کدهای کامل مثال‌های این سری را از آدرس زیر هم می‌توانید دریافت کنید: (^)


به روز رسانی
کدهای قسمت جاری را به روز شده جهت استفاده از EF 6 و StructureMap 3 در VS 2013، از اینجا می‌توانید دریافت کنید:
EF_Sample07  
مطالب
نوشتن پرس و جو در Entity Framework‌ با استفاده از LINQ To Entity قسمت اول

موجودیت‌های زیر را در نظر بگیرید: 

public class Customer
{
    public Customer()
    {
        Orders = new ObservableCollection<Order>();
    }
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Family { get; set; }

    public string FullName
    {
        get
        {
            return Name + " " + Family;
        }
    }

    public virtual IList<Order> Orders { get; set; }
}
public class Product
{
    public Product()
    {
    }

    public Guid Id { get; set; }
    public string Name { get; set; }
    public int Price { get; set; }
}
public class OrderDetail
{
    public Guid Id { get; set; }
    public Guid ProductId { get; set; }
    public int Count { get; set; }
    public Guid OrderId { get; set; }
    public int Price { get; set; }

    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }

    public string ProductName
    {
        get
        {
            return Product != null ? Product.Name : string.Empty;
        }
    }
}
public class Order
{
    public Order()
    {
        OrderDetail = new ObservableCollection<OrderDetail>();
    }
    public Guid Id { get; set; }
    public DateTime Date { get; set; }

    public Guid CustomerId { get; set; }
    public virtual Customer Customer { get; set; }
    public virtual IList<OrderDetail> OrderDetail { get; set; }

    public string CustomerFullName
    {
        get
        {
            return Customer == null ? string.Empty : Customer.FullName;
        }
    }

    public int TotalPrice
    {
        get
        {
            if (OrderDetail == null)
                return 0;

            return
                OrderDetail.Where(orderdetail => orderdetail.Product != null)
                .Sum(orderdetail => orderdetail.Price*orderdetail.Count);
        }
    }
}

و نگاشت موجودیت ها: 

public class CustomerConfiguration : EntityTypeConfiguration<Customer>
{
    public CustomerConfiguration()
    {
        HasKey(c => c.Id);
        Property(c => c.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class ProductConfiguration : EntityTypeConfiguration<Product>
{
    public ProductConfiguration()
    {
        HasKey(p => p.Id);
        Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderDetailConfiguration : EntityTypeConfiguration<OrderDetail>
{
    public OrderDetailConfiguration()
    {
        HasKey(od => od.Id);
        Property(od => od.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}
public class OrderConfiguration: EntityTypeConfiguration<Order>
{
    public OrderConfiguration()
    {
        HasKey(o => o.Id);
        Property(o => o.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    }
}

و برای معرفی موجودیت‌ها به Entity Framwork کلاس StoreDbContext را به صورت زیر تعریف می‌کنیم:

public class StoreDbContext : DbContext
{
    public StoreDbContext()
        : base("name=StoreDb")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new CustomerConfiguration());
        modelBuilder.Configurations.Add(new OrderConfiguration());
        modelBuilder.Configurations.Add(new OrderDetailConfiguration());
        modelBuilder.Configurations.Add(new ProductConfiguration());
    }

    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    public DbSet<OrderDetail> OrderDetails { get; set; }
}

جهت مقدار دهی اولیه به database تستی یک DataBaseInitializer به صورت زیر تعریف می‌کنیم:

public class MyTestDb : DropCreateDatabaseAlways<StoreDbContext>
{
    protected override void Seed(StoreDbContext context)
    {
        var customer1 = new Customer { Name = "Vahid", Family = "Nasiri" };
        var customer2 = new Customer { Name = "Mohsen", Family = "Jamshidi" };
        var customer3 = new Customer { Name = "Mohsen", Family = "Akbari" };

        var product1 = new Product {Name = "CPU", Price = 350000};
        var product2 = new Product {Name = "Monitor", Price = 500000};
        var product3 = new Product {Name = "Keyboard", Price = 30000};
        var product4 = new Product {Name = "Mouse", Price = 20000};
        var product5 = new Product {Name = "Power", Price = 70000};
        var product6 = new Product {Name = "Hard", Price = 250000};

        var order1 = new Order
        {
            Customer = customer1, Date = new DateTime(2013, 1, 1),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 1, Price = product1.Price},
                new OrderDetail {Product = product2, Count = 1, Price = product2.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
            }
        };

        var order2 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 5),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 2, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 4, Price = product3.Price},
            }
        };

        var order3 = new Order
        {
            Customer = customer1,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product1, Count = 4, Price = product1.Price},
                new OrderDetail {Product = product3, Count = 5, Price = product3.Price},
                new OrderDetail {Product = product5, Count = 6, Price = product5.Price},
            }
        };

        var order4 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 9),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product3, Count = 1, Price = product3.Price},
                new OrderDetail {Product = product6, Count = 1, Price = product6.Price},
            }
        };

        var order5 = new Order
        {
            Customer = customer2,
            Date = new DateTime(2013, 1, 12),
            OrderDetail = new List<OrderDetail>
            {
                new OrderDetail {Product = product4, Count = 1, Price = product4.Price},
                new OrderDetail {Product = product5, Count = 2, Price = product5.Price},
                new OrderDetail {Product = product6, Count = 5, Price = product6.Price},
            }
        };

        context.Customers.Add(customer3);

        context.Orders.Add(order1);
        context.Orders.Add(order2);
        context.Orders.Add(order3);
        context.Orders.Add(order4);
        context.Orders.Add(order5);

        context.SaveChanges();
    }

و در ابتدای برنامه کد زیر را جهت مقداردهی اولیه به Database مان قرار می‌دهیم:

Database.SetInitializer(new MyTestDb());

در انتها ConnectionString را در App.Config به صورت زیر تعریف می‌کنیم:

<connectionStrings>
    <add name="StoreDb" connectionString="Data Source=.\SQLEXPRESS;
Initial Catalog=StoreDBTest;Integrated Security = true" providerName="System.Data.SqlClient"/>
</connectionStrings>

بسیار خوب، حالا همه چیز محیاست برای اجرای اولین پرس و جو:

using (var context = new StoreDbContext())
{
    var query = context.Customers;

    foreach (var customer in query)
    {
        Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                  customer.Name, customer.Family);
    }
}

پرس و جوی تعریف شده لیست تمام Customer‌ها را باز می‌گرداند. query فقط یک "عبارت" پرس و جو هست و زمانی اجرا می‌شود که از آن درخواست نتیجه شود. در مثال بالا این درخواست در اجرای حلقه foreach اتفاق می‌افتد و درست در این لحظه است که دستور SQL ساخته شده و به Database فرستاده می‌شود. EF در این حالت تمام داده‌ها را در یک لحظه باز نمی‌گرداند بلکه این ارتباط فعال است تا حلقه به پایان برسد و تمام داده‌ها از database واکشی شود. خروجی به صورت زیر خواهد بود:

Customer Name: Vahid, Customer Family: Nasiri
Customer Name: Mohsen, Customer Family: Jamshidi
Customer Name: Mohsen, Customer Family: Akbari

نکته: با هر بار درخواست نتیجه از query ، پرس و جوی مربوطه دوباره به database فرستاده می‌شود که ممکن است مطلوب ما نباشد و باعث افت سرعت شود. برای جلوگیری از تکرار این عمل کافیست با استفاده از متد ToList پرس و جو را در لحظه تعریف به اجرا در آوریم
var customers = context.Customers.ToList();

خط بالا دیگر یک عبارت پرس و جو نخواهد بود بلکه لیست تمام Customer هاست که به یکباره از database بازگشت داده شده است. در ادامه هرجا که از customers استفاده کنیم دیگر پرس و جویی به database فرستاده نخواهد شد.

پرس و جوی زیر مشتریهایی که نام آنها Mohsen هست را باز می‌گرداند:

private static void Query3()
{
    using (var context = new StoreDbContext())
    {
        var methodSyntaxquery = context.Customers
                   .Where(c => c.Name == "Mohsen");
        var sqlSyntaxquery = from c in context.Customers
                             where c.Name == "Mohsen"
                             select c;

        foreach (var customer in methodSyntaxquery)
        {
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", 
                                      customer.Name, customer.Family);
        }
    }

    // Output:
    // Customer Name: Mohsen, Customer Family: Jamshidi
    // Customer Name: Mohsen, Customer Family: Akbari
}

همانطور که مشاهده می‌کنید پرس و جو به دو روش Method Syntax و Sql Syntax نوشته شده است.

روش Method Syntax روشی است که از متدهای الحاقی (Extention Method)  ‌و عبارت‌های لامبدا (Lambda Expersion) برای نوشتن پرس و جو استفاده می‌شود. اما #C روش Sql Syntax را که همانند دستورات SQL هست، نیز فراهم کرده است تا کسانیکه آشنایی با این روش دارند، از این روش استفاده کنند. در نهایت این روش به Method Syntax تبدیل خواهد شد بنابراین پیشنهاد می‌شود که از همین روش استفاده شود تا با دست و پنجه نرم کردن با این روش، از مزایای آن در بخشهای دیگر کدنویسی استفاده شود.

اگر به نوع Customers که در DbContext تعریف شده است، دقت کرده باشید، خواهید دید که DbSet می‌باشد. DbSet کلاس و اینترفیس‌های متفاوتی را پیاده سازی کرده است که در ادامه با آنها آشنا خواهیم شد:

  • IQueryable<TEntity>, IEnumerable<TEntity>, IQueryable, IEnumerable: که امکان استفاده از متدهای نام آشنای LINQ را برای ما فراهم می‌کند. البته فراموش نشود که EF از Provider ای با نام LINQ To Entity برای تفسیر پرس و جوی ما و ساخت دستور SQL متناظر آن استفاده می‌کند. بنابراین تمامی متدهایی که در LINQ To Object استفاده می‌شوند در اینجا قابل استفاده نیستند. بطور مثال اگر در پرس و جو از LastOrDefault روی Customer استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود. 
LINQ to Entities does not recognize the method 'Store.Model.Customer LastOrDefault[Customer](System.Linq.IQueryable`1[Store.Model.Customer], System.Linq.Expressions.Expression`1[System.Func`2[Store.Model.Customer,System.Boolean]])' method, and this method cannot be translated into a store expression.
  • <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local می‌باشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده می‌شوند که  در ادامه توضیح داده خواهند شد.
  • <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include می‌باشد و در ادامه توضیح داده خواهند شد.

متد Find: این متد کلید اصلی را به عنوان ورودی گرفته و برای بازگرداندن نتیجه مراحل زیر را طی می‌کند:
  1. داده‌های موجود در حافظه را بررسی می‌کند یعنی آنهایی که Load و یا Attach شده اند.
  2. داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
  3. داده هایی که در database هستند ولی هنوز Load نشده اند.
Find در صورت پیدا نکردن Exception ای صادر نمی‌کند بلکه مقدار null را بر می‌گرداند.
private static void Query4()
{
    using (var context = new StoreDbContext())
    {
        var customer = context.Customers.Find(new Guid("2ee2fd32-e0e9-4955-bace-1995839d4367"));

        if (customer == null)
            Console.WriteLine("Customer not found");
        else
            Console.WriteLine("Customer Name: {0}, Customer Family: {1}", customer.Name, customer.Family);
    }
}
با توجه به اینکه Id‌ها توسط Database ساخته می‌شوند. شما باید از Id دیگری که موجود می‌باشد، استفاده کنید تا نتیجه ای برگشت داده شود.
نکته: در صورتیکه کلید اصلی شما از دو یا چند فیلد تشکیل شده بود. می‌بایست این دو یا چند مقدار را به عنوان پارامتر به Find بفرستید.

متد Single: گاهی نیاز هست که داده‌ای پرس و جو شود اما نه با کلید اصلی بلکه با شرط دیگری، در این حالت از Single استفاده می‌شود. این متد یک مقدار را باز می‌گرداند و در صورتی که صفر یا بیش از یک مقدار در شرط صدق کند exception صادر می‌کند. متد SingleOrDefault رفتاری مشابه دارد اما اگر مقداری در شرط صدق نکند مقدار پیش فرض را باز می‌گرداند.
نکته: مقدار پیش فرض بستگی به نوع خروجی دارد که اگر object باشد مقدار null و اگر بطور مثال نوع عددی باشد، صفر می‌باشد.
private static void Query5()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.Single(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        try
        {
            var customer2 = context.Customers.Single(c => c.Name == "Mohsen");  // Exception: Sequence contains more than one element
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer3 = context.Customers.SingleOrDefault(c => c.Name == "Unkown");  // customer3 == null

        var customer4 = context.Customers.Single(c => c.Name == "Vahid");  // customer4 != null
    }
}

متد First:
در صورتیکه به اولین نتیجه پرس و جو نیاز هست می‌توان از First استفاده کرد. اگر پرس و جو نتیجه در بر نداشته باشد یعنی null باشد exception صادر خواهد شد اما اگر FirstOrDefault استفاده شود مقدار پیش فرض برگردانده خواهد شد.
private static void Query6()
{
    using (var context = new StoreDbContext())
    {
        try
        {
            var customer1 = context.Customers.First(c => c.Name == "Unkown");  // Exception: Sequence contains no elements
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
        }

        var customer2 = context.Customers.FirstOrDefault(c => c.Name == "Unknown");  // customer2 == null
        var customer3 = context.Customers.First(c => c.Name == "Mohsen");
    }
}

مطالب
سری فیبوناچی و دات نت 4 !

سری معروف فیبوناچی که معرف حضور شما هست. سری از اعداد است که هر عدد آن مساوی حاصل جمع دو عدد ماقبل آن است. دو عدد اول این سری هم 0 و 1 هستند.
اگر بخواهیم این الگوریتم را به صورت یک متد بازگشتی نمایش دهیم به صورت زیر خواهد بود:

public static int Fibonacci(int x)
{
if (x <= 1)
return 1;
return Fibonacci(x - 1) + Fibonacci(x - 2);
}

این الگوریتم چند مشکل دارد:
الف) برای اعداد بزرگ حتی با بکارگیری Int64 و یا double و امثال آن هم باز به جواب نخواهیم رسید (برای مثال 1500 را بررسی کنید).
ب) بسیار کند است.

در دات نت 4 برای کار با اعداد بزرگ، فضای نام System.Numerics معرفی شده است که حاوی نوع جدیدی از اعداد به نام BigInteger است.
اکنون اگر الگوریتم سری فیبوناچی را بر اساس این نوع داده جدید بازنویسی کنیم خواهیم داشت:

using System;
using System.Collections.Generic;
using System.Numerics; //needs a ref. to this assembly

namespace Fibonaci
{
public class CFibonacci
{
public static int Fibonacci(int x)
{
if (x <= 1)
return 1;
return Fibonacci(x - 1) + Fibonacci(x - 2);
}

public static IEnumerable<BigInteger> BigFib(Int64 toNumber)
{
BigInteger previous = 0;
BigInteger current = 1;

for (Int64 y = 1; y <= toNumber; y++)
{
var auxiliar = current;
current += previous;
previous = auxiliar;
yield return current;
}
}
}
}
و مثالی در مورد نحوه استفاده از آن:

using System;
using System.Linq;

namespace Fibonaci
{
class Program
{
static void Main()
{
foreach (var i in CFibonacci.BigFib(10))
{
Console.WriteLine("{0}", i);
}

var num = 12000;
var fib = CFibonacci.BigFib(num).Last();
Console.WriteLine("fib({0})={1}", num, fib);

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

برای نمونه با عدد 12000 خروجی برنامه در کسری از ثانیه (و نه چند دقیقه یا ساعت) به شرح زیر خواهد بود:

fib(12000)=514263424911336592579396579289954520826834443526829600435873863248622
65414020714013892551476261070010099275571144059579167356039437242089427136323689
02207956221569622791450891447905907668251232675988098246382902426783148546665404
47372384043164600945249911273857878346679362876357499204290285069442042444471200
52292329349103672302428662317285015525888210397583707071480178840772972692357054
71823998861896761687119434646250991702691100894769561810834542099577336821493905
41651658937860506067011215222435859797671748514023462634575877112541265857011723
31453990415231608729534781720381122965899871018532003735284559342372552627132300
63895825396012087948050855095233633638445668687440232926253620457459973889510838
23542785159371236389909470974738599166720611351903568781845409425624666559791912
02212289710838873334773835118391287956725504426150461421914844191810523257658770
99885492757927034409234340065928400769741802132203888929463702342324148343605275
28928280472094493359682662519127203581813404104542972181231076224891404730611459
03321942693225066038987483163709402601230467054944349111055850348779989058517069
96087626795709205215727843443054577680024507650678240240742421270422674907476927
22422733945450760323640619100021663675080870429299040891840880753646474330069332
72320218334582268219906763463261387161318500503970491314781100556494361063341371
43577787961183154125538371204296752028496084633103476783071177779604042581017888
28257784920659671082363171157289668904381254080676855815524987553372657063695970
39668109161449140707240711279859427919912443872405284305891366802954763421905970
15206311458187449420118838775707435857999310870199585760807680179258273461000460
97527064929564528474349547038178370043823628944670926601955537657427194815893365
88494863101667547896798728140224921584809355334379707156342620570496834086358692
30946467203330676206265047960072392991634456381998479411463182171816379650120684
35082399788137090460167819041845511951296934273988759169877839532492294430334328
46972905198131530224288922834125154211248159843609629469051889033085360540770480
25633451201705370447586177546577777759300410144166197439355903631773088812515215
09638377918595294747887970034209028019490210394392422302403687059119407005858379
52137098994457236290005745735420803758853723206992134642997705010940581386168427
47382973672816710014652632509888958851675894223117421829434728942878605569971512
65291783384910157203679779458354245579846973830472593370160977523707902575129803
072039857524154149354311250529579592001

مطالب
برنامه نویسی اندروید با Xamarin.Android - قسمت سوم
در این مقاله می‌خواهیم یک لیست ساده را ایجاد کرده و داخل یک کنترل (View)، از نوع ListView قرار دهیم. همچنین با برخی از کنترل‌های پرکاربرد، برای چیدمان کنترل‌ها در اندروید آشنا می‌شویم.

قبل از شروع به طراحی UI باید کمی با واحدهای اندازه گیری در اندروید آشنا شویم. بدانید و آگاه باشید که استفاده از واحد Pixel برای تعیین اندازه در اندروید کار بسیار اشتباهی است. طراح همیشه باید Density یا تراکم صفحه‌ی نمایش را در نظر بگیرد. تراکم صفحه‌ی نمایش به معنای تعداد پیکسل موجود در یک اینچ می‌باشد. اندازه‌ی 100 پیکسل در دستگاه‌های مختلف با (dpi(Dot Per Inchهای متفاوت به یک اندازه نیست.

واحد dpi: اندروید واحد dpi را برای طراحی و چیدمان Layoutها معرفی کرده است. dpi مخفف Device Independent Pixel هست و معمولا بصورت dp نوشته می‌شود که یک واحد پیکسلی مجازی است و بر پایه‌ی یک صفحه نمایش با رزولوشن 160dpi طراحی شده‌است. به عبارت دیگر یک dp، یک پیکسل در یک صفحه‌ی نمایش با رزولوشن 160dpi می‌باشد. این واحد این اطمینان را به شما می‌دهد که یک View، در صفحه نمایش‌های با رزولوشن متفاوت، بطور مناسبی بزرگ یا کوچک می‌شود.

واحد sp: مخفف Scale Independent Pixel است و شبیه dp عمل می‌کند؛ با این تفاوت که تنظیمات کاربر را (مثلا شخصی که بخاطر ضعف چشم اندازه‌ی قلم گوشی خود را بزرگ نموده) در محاسبات خود در نظر می‌گیرد. به دلیل آنکه از لحاظ زیبایی شناسی و همچنین چیدمان عناصر داخل UI زمانیکه از واحد اندازه گیری sp استفاده می‌کنیم ممکن است با مشکل مواجه شویم، بیشتر از dp استفاده می‌کنیم، مگر در بعضی مواقع آن هم برای مقداردهی به اندازه‌ی قلم!

خوب! به سراغ فولدر Layout رفته و Main.axml را باز نمایید. به قسمت Source بروید.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">
    <Button
        android:id="@+id/MyButton"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/Hello" />
</LinearLayout>
در این سند axml یک LinearLayout مشاهده می‌نمایید. وقتی شما View را به LinearLayout اضافه می‌کنید، با توجه به اینکه orientation آن را vertical یا horizontal انتخاب کرده باشید، به صورت افقی و یا عمودی طرح بندی را انجام می‌دهد.

layout_width و layout_height (مقداردهی آن‌ها الزامی است) ابعاد layout ما را مشخص می‌کنند. مقدار fill_parent دیگر منسوخ شده و به جای آن match_parent استفاده می‌شود و به معنای آن است که تمام فضای موجود در کنترل را اشغال کند. مقدار دیگری که می‌توان به آن نسبت داد (و در layout_height مربوط به Button مشاهده می‌نمایید)، wrap_content می‌باشد که اعلام می‌کند فقط به میزان مورد نیاز برای محتویات، کنترل والد را اشغال کند. البته با تغییر میزان محتویات، اندازه‌ی کنترل متغییر است. شما می‌توانید مقادیر عددی را هم با واحد dp یا حتی pixel (که اصلا توصیه نمی‌شد) جایگزین نمایید.

در ادامه، کنترل (که در اندروید به آن View گفته می‌شود) Button را حذف نمایید و به جای آن یک ListView را قرار دهید و نامی را به آن نسبت دهید. ListView از کاربردی‌ترین و مهم‌ترین کنترل‌های اندروید می‌باشد. ListView شامل قسمت‌های زیر است:
Rows: قسمت نمایش دهنده‌ی داده‌ها.
Adapter: یک کلاس که وظیفه‌ی انقیاد منبع داده را به ListView، بر عهده دارد.
Fast Scrolling: یک دسته(handle) که به کاربر اجازه می‌دهد تا در طول ListView حرکت کند.
Section Index: یک view می‌باشد و جایگاه لیت را هنگام اسکرول مشخص میکند و معمولا در Contacts گوشی بصورت ابتدای حروف نام مخاطبین خود مشاهده کرده‌اید.
Layout زیر را در نظر بگیرید:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:background="#fff"
        android:id="@+id/NameListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>  
به MainActivity.cs بروید و کدهای مربوط به Button قبلی را که با ListView جایگزین کرده‌ایم، حذف نمایید. متد OnCreate به این صورت می‌باشد:
protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            List<string> namesList = new List<string>
            {
                "Mohammad","Fatemeh","Ali","Hasan","Husein","Mohsen","Mahdi",
            };
            var namesAdapter = new ArrayAdapter<string>
                (this, Android.Resource.Layout.SimpleListItem1, namesList);

            var listview = FindViewById<ListView>(Resource.Id.NameListView);
            listview.Adapter = namesAdapter;
        }
همانطور که گفته شد SetContentView مشخص کننده‌ی layout مورد نظر ما برای نمایش می‌باشد. می‌توان بدون هیچ layout خاصی با کدهای سی شارپ، کنترل‌های مورد نظر را ایجاد کرد که کار زمانبری است؛ ولی بعضی مواقع مجبور به این کار هستیم.
namesList یک لیست ساده از نوع string با مقدار دهی اولیه است.
ArrayAdapter یک کلاس Adapter توکار می‌باشد که یک آرایه (یا لیست) را از نوع string، برای نمایش به ListView متصل می‌کند (bind). نوع جنریک آن یعنی <ArrayAdapter<T برای نوع‌های دیگر هم استفاده می‌شود. در واقع Adapter با دریافت یک لیست برای نمایش و یک Layout برای تعیین نوع نمایش، به ازای هر سطر از اطلاعات یک View را با اطلاعات آن سطر به سمت ListView ارسال می‌کند. در اینجا ما در سازنده‌ی ArrayAdapter با استفاده از Resourceهای توکار اندروید که از طریق Android.Resource به آن‌ها دسترسی داریم، یک layout ساده را شامل یک TextView(مانند label و یا textBlock)، به همراه namesList، برای Adapter ارسال کردیم.
متد FindViewById با توجه به Layout معرفی شده‌ی به Activity، به دنبال View با Id مورد نظر می‌پردازد. مهم نیست که در Layoutهای جداگانه نام‌های یکسانی استفاده کنید. این متد در کلاس View قرار دارد و تمام کنترل(View)ها، فرزند آن می‌باشند. در اینجا از نوع جنریک آن استفاده شده که عمل تبدیل View به ListView را خود متد بر عهده بگیرد.
در انتها Adapter مورد نظر به ویژگی Adpater کنترل ListView اضافه می‌شود.

ListView کنترل بسیار منعطفی می‌باشد. برخی ویژگی‌ها آن را در زیر می‌توانید مشاهده بفرمایید:
  • android:dividerHeight                    // ارتفاع جداکننده‌ی سطرها
  • android:divider                            // رنگ جداکننده‌ی سطرها
  • android:layoutAnimation               // انیمیشن برای layoutها 
  • android:background                    // رنگ ضمینه را مشخص میکند. البته میتوانید یک style را به ان نسبت دهید

خوب؛ حالا بیایید یک ListView را با ظاهر و Adapter سفارشی بسازیم.
ابتدا باید یک Layout را طراحی کنیم تا به ازای هر سطر برای ListView ارسال شود. با استفاده از Add->New item یک Layout را به فولدر layout اضافه کنید.
کد زیر را درون فایل axml مربوطه کپی کنید. 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="14dp">
    <TextView
        android:text=""
        android:gravity="center_vertical"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:id="@+id/idTextView" />
    <TextView
        android:text=""
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/nameTextView"
        android:layout_marginLeft="14dp" />
</LinearLayout>
کلاس زیر (یا هر کلاس دلخواه دیگری) را به عنوان مدل برنامه اضافه کنید.
namespace DotSystem.ir.App1.Model
{
    public class Person
    {
        public int Id { get; set; }
        public string PersonName { get; set; }

    }
حالا باید Adapter خود را بسازیم. ابتدا کلاسی را با نام PersonAdapter به برنامه اضافه نمایید. این کلاس باید از کلاس BaseAdapter (نوع جنریک آن هم موجود می‌باشد) و یا فرزندان آن ArrayAdapter، CursorAdapter و ... ارث بری نماید. اگر مستقیما از BaseAdapter استفاده کنیم، به دلیل Abstract بودن تعدادی از متدها و Propertyها مجبور به override کردن آن‌ها می‌شویم. ما در اینجا از BaseAdapter استفاده می‌کنیم. کد زیر را در نظر بگیرید:
namespace DotSystem.ir.App1.Adapters
{
    public class PersonAdapter : BaseAdapter<Model.Person>
    {
        public override Person this[int position]
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override int Count
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        public override long GetItemId(int position)
        {
            throw new NotImplementedException();
        }

        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            throw new NotImplementedException();
        }
    }
}
BaseAdapter شامل یک Indexer برای دسترسی آسان به Itemهای لیست، یک ویژگی برای برگرداندن تعداد آیتم‌ها، متدی برای برگرداندن Id هر آیتم و مهمترین بخش آن یعنی متد GetView که برای نمایش هر آیتمی یک بار اجرا می‌شود و Layout مورد نظر ما را با اطلاعات پر کرده و به سمت ListView می‌فرستد.

در اینجا ما به چند فیلد داخل کلاس احتیاج داریم.
  • لیست اطلاعات مورد نظر.
  • Activity جاری که Adapter را استفاده می‌کند.
بنابراین دو فیلد را به همراه متد سازنده، برای مقدار دهی آن‌ها اضافه کرده و کلاس بالا را نیز تکمیل می‌کنیم.
namespace DotSystem.ir.App1.Adapters
{
    public class PersonAdapter : BaseAdapter<Person>
    {
        protected Activity _activity = null;
        protected List<Person> _list = null;
        public PersonAdapter(Activity activity, List<Person> list)
        {
            _activity = activity;
            _list = list;
        }
        public override Person this[int position]
        {
            get
            {
                return _list[position];
            }
        }

        public override int Count
        {
            get
            {
                return _list.Count;
            }
        }

        public override long GetItemId(int position)
        {
            return _list[position].Id;
        }

        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            throw new NotImplementedException();
        }
    }
}
در این مرحله باید متد GetView را پیاده سازی کنیم. به پیاده سازی زیر دقت کنید:
public override View GetView(int position, View convertView, ViewGroup parent)
        {
            if (convertView == null)
                convertView = _activity.LayoutInflater
                    .Inflate(Resource.Layout.PersonListViewItemLayout, parent, false);

            var idTextView = convertView.FindViewById<TextView>(Resource.Id.idTextView);
            var nameTextView = convertView.FindViewById<TextView>(Resource.Id.NameListView);

            var persion = _list[position];

            idTextView.Text = persion.Id.ToString();
            nameTextView.Text = persion.PersonName;

            return convertView;
        }
در مرحله‌ی اول بررسی می‌کنیم که اگر convertView برابر با null بود، آن را مقدار دهی کند. این نکته بسیار مهم است، چرا که ListView برای کارآیی بهتر فقط آن آیتم هایی را که در دید کاربر باشد، با متد GetView لود میکند و دوباره با اسکرول لیست، عمل فراخوانی متد انجام می‌شود؛ البته اینبار بدون مقدار null برای convertView. بنابراین اگر دیدید که هنگام اسکرول لیست، آیتم‌ها جابجا شدند، این بخش از متد را دوباره بررسی نمایید.
Inflate متدی است که Layout و نگه دارنده‌ی  layout را گرفته و آن را برای نمایش در Activity آماده می‌کند. سپس دو View را که در Layout ما وجود دارند، گرفته مقدار دهی می‌کنیم و در آخر هم convertView را برای نمایش به سمت ListView می‌فرستیم.
حال متد OnCreate را به صورت زیر بازنویسی نموده و برنامه را اجرا می‌کنیم.
protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);

            List<Model.Person> personList = new List<Model.Person>
            {
                new Model.Person() {Id = 1, PersonName = "Mohammad", },
                new Model.Person() {Id = 2, PersonName = "Ali", },
                new Model.Person() {Id = 3, PersonName = "Fatemeh", },
                new Model.Person() {Id = 4, PersonName = "hasan", },
                new Model.Person() {Id = 5, PersonName = "Husein", },
                new Model.Person() {Id = 6, PersonName = "Mohsen", },
                new Model.Person() {Id = 14, PersonName = "Mahdi", },
            };
            var personAdapter = new Adapters.PersonAdapter(this, personList);

            var listview = FindViewById<ListView>(Resource.Id.NameListView);
            listview.Adapter = personAdapter;
        }
مطالب
ارسال PingBack در ASP.NET
Pingback یکی از روش‌های اطلاع رسانی به سایت‌های دیگر در مورد لینک دادن به آن‌ها در سایت خود است. برای مثال من لینکی از یکی از مطالب شما را در متن جاری خودم قرار می‌دهم. سپس به وسیله‌ی ارسال یک ping، در مورد انجام اینکار به شما اطلاع رسانی می‌کنم. حاصل آن عموما قسمت معروف ping-backs سایت‌ها است. این مورد نیز یکی از روش‌های مؤثر SEO در گرفتن backlink است و تبلیغ محتوا.
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام می‌شود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار می‌گیرد؛ بجای استفاده از پروتکل‌های استاندارد وب سرویس‌ها مانند Soap و امثال آن. پیاده سازی‌های ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شده‌است. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامه‌های ASP.NET.


یافتن آدرس وب سرویس سایت پذیرای Pingback

اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه می‌شود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
 <link rel="pingback" href="pingback server">
برای مثال اگر به وبلاگ‌های MSDN دقت کنید، هدر x-pingback را می‌توانید در خروجی وب سرور آن‌ها مشاهده کنید:


همانطور که ملاحظه می‌کنید، نیاز است Response header را آنالیز کنیم.
        private Uri findPingbackServiceUri()
        {
            var request = (HttpWebRequest)WebRequest.Create(_targetUri);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Get;
            request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
            using (var response = request.GetResponse() as HttpWebResponse)
            {
                if (response == null) return null;

                var url = extractPingbackServiceUriFormHeaders(response);
                if (url != null)
                    return url;

                if (!isResponseHtml(response))
                    return null;

                using (var reader = new StreamReader(response.GetResponseStream()))
                {
                    return extractPingbackServiceUriFormPage(reader.ReadToEnd());
                }
            }
        }

        private static Uri extractPingbackServiceUriFormHeaders(WebResponse response)
        {
            var pingUrl = response.Headers.AllKeys.FirstOrDefault(header =>
                                header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) ||
                                header.Equals("pingback", StringComparison.OrdinalIgnoreCase));

            return getValidAbsoluteUri(pingUrl);
        }

        private static Uri extractPingbackServiceUriFormPage(string content)
        {
            if (string.IsNullOrWhiteSpace(content)) return null;
            var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase);
            var match = regex.Match(content);
            return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value);
        }

        private static Uri getValidAbsoluteUri(string url)
        {
            Uri absoluteUri;
            return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri;
        }

        private static bool isResponseHtml(WebResponse response)
        {
            var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header =>
                                        header.Equals("content-type", StringComparison.OrdinalIgnoreCase));
            return !string.IsNullOrWhiteSpace(contentTypeKey) &&
                    response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase);
        }
نحوه‌ی استخراج آدرس سرویس Pingback یک سایت را در کدهای فوق ملاحظه می‌کنید.
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شده‌است. زمانیکه این صفحه را درخواست می‌کنیم، response.Headers.AllKeys حاصل می‌تواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان می‌یابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شده‌است.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شده‌است. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.


ارسال Ping به آدرس سرویس Pingback

اکنون که آدرس سرویس pingback یک سایت را یافته‌ایم، کافی است ping ایی را به آن ارسال کنیم:
        public void Send()
        {
            var pingUrl = findPingbackServiceUri();
            if (pingUrl == null)
                throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host));

            sendPing(pingUrl);
        }

        private void sendPing(Uri pingUrl)
        {
            var request = (HttpWebRequest)WebRequest.Create(pingUrl);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Post;
            request.ContentType = "text/xml";
            request.ProtocolVersion = HttpVersion.Version11;
            makeXmlRpcRequest(request);
            using (var response = (HttpWebResponse)request.GetResponse())
            {
                response.Close();
            }
        }

        private void makeXmlRpcRequest(WebRequest request)
        {
            var stream = request.GetRequestStream();
            using (var writer = new XmlTextWriter(stream, Encoding.ASCII))
            {
                writer.WriteStartDocument(true);
                writer.WriteStartElement("methodCall");
                writer.WriteElementString("methodName", "pingback.ping");
                writer.WriteStartElement("params");

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteEndElement();
                writer.WriteEndElement();
            }
        }
اینبار HttpWebRequest تشکیل شده از نوع post است و نه get. همچنین مقداری را که باید ارسال کنیم نیاز است مطابق پروتکل XML-RPC باشد. برای کار با XML-RPC در دات نت یا می‌توان از کتابخانه‌ی Cook Computing's XML-RPC.Net استفاده کرد و یا مطابق کدهای فوق، دستورات آن‌را توسط یک XmlTextWriter کنار هم قرار داد و نهایتا در درخواست Post ارسالی درج کرد.
در اینجا sourceUri آدرس صفحه‌ای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شده‌است. در یک pinback، صرفا این دو آدرس به سرویس دریافت کننده‌ی pingback ارسال می‌شوند.
سپس سایت دریافت کننده‌ی ping، ابتدا sourceUri را دریافت می‌کند تا عنوان آن‌را استخراج کند و همچنین بررسی می‌کند که آیا targetUri، در آن درج شده‌است یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs


نحوه‌ی استفاده از کلاس Pingback تهیه شده

کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش می‌شوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینک‌های خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینک‌های یک محتوا را نیز می‌توان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانه‌ی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url);
var linkTags = doc.DocumentNode.Descendants("link");
var linkedPages = doc.DocumentNode.Descendants("a")
                                  .Select(a => a.GetAttributeValue("href", null))
                                  .Where(u => !String.IsNullOrEmpty(u));
نظرات مطالب
ASP.NET MVC #20
- متد index یک چنین امضایی دارد
public ActionResult Index(string sort, string sortdir, int? page = 1)
زمانیکه این اطلاعات را دارید، برای مثال از LINQ استفاده کرده و با استفاده از متدهای Take ، Skip و OrderBy کار بازیابی قطعه‌ای از اطلاعات مورد نظر را انجام دهید.
- تعداد کل سطرها را هم کوئری بگیرید جداگانه و کش کنید. روش برای ارسال آن به یک View همانند کلیه روش‌های قابل استفاده در MVC است.
مطالب
قابلیت چند زبانه و Localization در AngularJs- بخش چهارم و نهایی: Best Practiceهای angular-translate
در بخش پیشین چند مورد از قابلیت‌های angular-translate را بررسی نمودیم. در این بخش به بررسی باقی موارد می‌پردازیم.

ex7_load_static_files 

در این مثال خواهیم دید که چگونه یک فایل translate table در موقع فراخوانی به صورت On Demand بارگذاری خواهد شد. در قدم اول اسکریپت‌های زیر به صفحه افزوده می‌شوند.
    <script src="Scripts/angular.js"></script>
    <script src="Scripts/angular-cookies.js"></script>
    <script src="Scripts/angular-translate.js"></script>
    <script src="Scripts/angular-translate-storage-cookie.js"></script>
    
    <!-- for override loader methods in angular translate -->
    <script src="/src/service/loader-static-files.js"></script>
در ادامه درباره‌ی اسکریپت پنجم بیشتر توضیح خواهیم داد. بگذارید از آخر به اول شروع کنیم و ببینیم که نحوه‌ی فراخوانی و استفاده از امکان on demand بارگذاری شدن فایل‌های زبان به چه صورتی می‌باشد. در زیر، تکه کد اصافه شده به ex7 را مشاهده می‌کنید:
            // Register a loader for the static files
            // So, the module will search missing translation tables under the specified urls.
            // Those urls are [prefix][langKey][suffix].
            $translateProvider.useStaticFilesLoader({
                prefix: '/l10n/',
                suffix: '.json'
            });
همانطور که در توضیحات آمده است، ماژول با دریافت prefix و suffix که در حقیقت همان فولدر و پسوند فایل‌های translate table هستند، هر زبانی را که مورد نیاز است و تا کنون بارگذاری نشده، بارگذاری می‌نماید. تصویر زیر محتویات فولدر l10n را نمایش می‌دهد.

حال ببینیم که این فرآیند در loader-static-files چگونه پیاده سازی شده است. در این فایل یک متد load نوشته شده است که فایل‌های static را طبق یک الگوی مشتمل بر prefix و suffix از سرور می‌خواند. لزومی ندارد که شما فایل‌ها را حتما به صورت JSON و با این پسوند ذخیره کنید. اما چیزی که قطعی است این است که فایل‌ها حتما باید به صورت key value ذخیره شده باشند.

تکه کد زیر اطلاعات فایل loader-static-files را نمایش می‌دهد.

angular.module('pascalprecht.translate')
.factory('$translateStaticFilesLoader', $translateStaticFilesLoader);
function $translateStaticFilesLoader($q, $http) {
    
  'use strict';

  return function (options) {

    if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) {
      throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!');
    }

    if (!options.files) {
      options.files = [{
        prefix: options.prefix,
        suffix: options.suffix
      }];
    }

    var load = function (file) {
      if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) {
        throw new Error('Couldn\'t load static file, no prefix or suffix specified!');
      }

      var deferred = $q.defer();

      $http(angular.extend({
        url: [
          file.prefix,
          options.key,
          file.suffix
        ].join(''),
        method: 'GET',
        params: ''
      }, options.$http)).success(function (data) {
        deferred.resolve(data);
      }).error(function () {
        deferred.reject(options.key);
      });

      return deferred.promise;
    };

    var deferred = $q.defer(),
        promises = [],
        length = options.files.length;

    for (var i = 0; i < length; i++) {
      promises.push(load({
        prefix: options.files[i].prefix,
        key: options.key,
        suffix: options.files[i].suffix
      }));
    }

    $q.all(promises).then(function (data) {
      var length = data.length,
          mergedData = {};

      for (var i = 0; i < length; i++) {
        for (var key in data[i]) {
          mergedData[key] = data[i][key];
        }
      }

      deferred.resolve(mergedData);
    }, function (data) {
      deferred.reject(data);
    });

    return deferred.promise;
  };
}

$translateStaticFilesLoader.displayName = '$translateStaticFilesLoader';

همانطور که ملاحظه می‌کنید، کد فوق یک سرویس با نام $translateStaticFilesLoader را تعریف نموده است. در صورتیکه ما در کنترلر فایل ex7، اصلا نامی از آن نبردیم و تنها از $translateProvider.useStaticFilesLoader استفاده نمودیم! جواب در نحوه‌ی نگارش کد angular-translate نهفته است. در خط 866 فایل angular-translate تکه کد زیر مربوط به تعریف translateStaticFileLoader می‌باشد. همانطور که ملاحظه می‌کنید سرویس translateStaticFilesLoader درون فضای نام سرویس translateTable قرار گرفته است. بنابراین ما تنها با تعریف سرویس translateStaticFilesLoader، در حقیقت آن را override نموده‌ایم. در کد نمونه‌ای که در بخش‌های قبلی قرار داده‌ام یک فایل translate.js نیز قرار دارد که در فولدر src/services قرار گرفته است. این فایل نیز برخی از امکانات و سرویس‌های built-in درون angular-translate را سفارشی نموده است.

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader
   * @methodOf pascalprecht.translate.$translateProvider
   *
   * @description
   * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader.
   *
   * @param {Object=} options Optional configuration object
   */
  this.useStaticFilesLoader = function (options) {
    return this.useLoader('$translateStaticFilesLoader', options);
  };

در این 4 مجموعه سعی کردم تمامی آنچه را که برای ایجاد قابلیت چند زبانه و localization نیاز است و حیاتی بود، تشریح کنم. بنابراین تا کنون دانش خوبی درباره‌ی این کتابخانه کسب نموده‌اید. باقی تمرین‌ها را می‌توانید بر حسب نیاز با استفاده از مستندات موجود در angular-translate مطالعه و استفاده نمایید.

مطالب
تبدیل زیرنویس‌های یوتیوب به فایل srt

تعدادی از ویدیوهای یوتیوب، خصوصا مواردی که از Google videos به یوتیوب منتقل شده‌اند، دارای زیر نویس هستند. این زیر نویس هم فرمت جالبی دارد:
<?xml version="1.0" encoding="UTF-8"?>
<transcript>
<text start="23.49" dur="5.97">
&amp;gt;&amp;gt;Commentator: We have here today, Steve with
us whom I met not too long ago at something
</text>
...
</transcript>
و از هر لحاظ بهتر است از فرمت srt متداول (خصوصا از لحاظ قابلیت بهتر parse آن) ولی ... این فرمت xml ایی، متداول نیست و بهتر است جهت استفاده از آن در برنامه‌های پخش کننده فایل‌های ویدیویی، به srt تبدیل شود.
ابتدا شاید بپرسید که این‌ها را چگونه می‌توان دریافت کرد. روش کار به صورت زیر است:
http://video.google.com/timedtext?hl=en&v=Id&lang=en

در قسمت‌های hl و lang ، زبان مورد نظر قرار می‌گیرد و قسمت v همان id معروف ویدیوی یوتیوب مورد نظر است. برای مثال اگر لینک ویدیوی ما http://www.youtube.com/watch?v=-PA-buwI3q4 باشد، لینک زیرنویس انگلیسی هماهنگ با آن http://video.google.com/timedtext?hl=en&v=-PA-buwI3q4&lang=en خواهد بود.

البته زمان‌های این فایل xml یک نکته‌ی خاص گوگلی دارد!
اگر عدد 3.4 را مشاهده کردید یعنی سه ثانیه و 400 میلی ثانیه
اگر عدد 3.04 را مشاهده کردید یعنی سه ثانیه و 40 میلی ثانیه
به عبارتی با قسمت اعشاری آن باید به صورت رشته برخورد کنید. اگر طول آن یک بود، دو صفر و اگر طول آن 2 بود، 1 صفر در جلوی آن باید قرار گیرد تا قابل استفاده شود.

کلاس تهیه شده زیر کار تبدیل Xml گوگل را به Srt انجام می‌دهد:
using System;
using System.Linq;
using System.Text;
using System.Web;
using System.Xml.Linq;

namespace YtSubs
{
public class GoogleXmlToSrt
{
public string ConvertXmlToSrt(string url)
{
var transcript = loadTranscript(url);

var srtBuf = new StringBuilder();
var lineNumber = 0;

foreach (var item in transcript.Root.Elements("text"))
{
lineNumber++;
srtBuf.Append(lineNumber.ToString());

var timeLine = getTimeLine(item);
srtBuf.AppendFormat("{0}{1} --> {2}{3}", Environment.NewLine, timeLine.Item1, timeLine.Item2, Environment.NewLine);

var msg = decodeText(item);
srtBuf.AppendLine(msg + Environment.NewLine);
}
return srtBuf.ToString();
}

private static XDocument loadTranscript(string url)
{
var transcript = XDocument.Load(url);

if (transcript.Root == null)
throw new InvalidOperationException("couldn't load received data.");
return transcript;
}

private static string decodeText(XElement text)
{
var textValue = text.Value.Split('\n');
var msg = textValue.Aggregate(
string.Empty, (current, line) =>
current + (HttpUtility.HtmlDecode(line.Trim()) + Environment.NewLine)
);
return msg.Trim();
}

private static int fractionalPart(string data)
{
var idx = data.IndexOf(".");
if (idx == -1) return 0;
var fractionalPart = data.Substring(idx + 1);
if (fractionalPart.Length == 1) //3.4 --> 3.400
return int.Parse(fractionalPart + "00");

if (fractionalPart.Length == 2) //3.04 --> 3.040
return int.Parse(fractionalPart + "0");

return int.Parse(fractionalPart.Substring(0, 3));
}

private Tuple<string, string> getTimeLine(XElement text)
{
var startTs = getStartTs(text);
TimeSpan endTs = getEndTs(text, startTs);
return new Tuple<string, string>(timeSpanToString(startTs), timeSpanToString(endTs));
}

private static TimeSpan getStartTs(XElement text)
{
var startData = text.Attribute("start");
if (startData == null)
throw new InvalidOperationException("This is not a valid subtitle file.");

var start = startData.Value;
var startTs = new TimeSpan(0, 0, 0, (int)Math.Truncate(double.Parse(start)), fractionalPart(start));
return startTs;
}

private static TimeSpan getEndTs(XElement text, TimeSpan startTs)
{
TimeSpan endTs;
var durData = text.Attribute("dur");
if (durData == null)
{
endTs = startTs + new TimeSpan(0, 0, 0, 3, 0);
}
else
{
var dur = durData.Value;
var durTs = new TimeSpan(0, 0, 0, (int)Math.Truncate(double.Parse(dur)), fractionalPart(dur));
endTs = startTs + durTs;
}
return endTs;
}

private static string timeSpanToString(TimeSpan lineTs)
{
return string.Format("{0}:{1}:{2},{3}",
lineTs.Hours.ToString("D2"),
lineTs.Minutes.ToString("D2"),
lineTs.Seconds.ToString("D2"),
lineTs.Milliseconds.ToString("D3"));
}
}
}
و یک نمونه از استفاده آن می‌تواند به شکل زیر باشد:
using System.IO;

namespace YtSubs
{
class Program
{
static void Main(string[] args)
{
var url = "http://video.google.com/timedtext?hl=en&v=-PA-buwI3q4&lang=en";
//var url = "subtitle.xml";
var srtBuf = new GoogleXmlToSrt().ConvertXmlToSrt(url);
File.WriteAllText("-PA-buwI3q4.srt", srtBuf);
}
}
}


مطالب تکمیلی:
همه چیز درباره زیرنویس ویدئوهای گوگل

مطالب
بررسی مفهوم Captured Variable در زبان سی شارپ
Capturing Outer Variables  
یک عبارت لامبدا می‌تواند از متغیرهای محلی و یا پارامترهای متدی که در آن تعریف شده است، استفاده نماید (Outer Variables). این متغیرها را captured variables می‌نامند. عبارت لامبدایی که از این متغیرها استفاده می‌کند، closure نامیده می‌شود. برای مثال:
static void Main()
{
 int factor = 2;
 Func<int, int> multiplier = n => n * factor;
 Console.WriteLine (multiplier (3)); // 6
}
در کد فوق multiplier یک delegate می‌باشد که ورودی صحیح n را گرفته و در مقدار factor ضرب کرده و بر می‌گرداند.

عبارت لامبدا زمانی ارزیابی می‌شود که delegate متناظر فراخوانی (Invoke) گردد؛ نه زمانیکه متغیر اصطلاحا capture می‌شود:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
در کد فوق در زمانی که multiplier فراخوانی می‌شود مقدار factor برابر 10 ارزیابی شده و لذا عدد 30 چاپ خواهد شد.

عبارات لامبدا خود می‌توانند captured variable‌ها را تغییر دهند:
int seed = 0;
Func<int> natural = () => seed++;
Console.WriteLine (natural()); // 0
Console.WriteLine (natural()); // 1
Console.WriteLine (seed); // 2
در کد فوق natural یک delegate بدون ورودی و با یک خروجی integer می‌باشد. در ابتدا متغیر محلی seed تعریف شده و با مقدار اولیه 0 مقداردهی می‌شود. با هر بار اجرای natural مقدار seed به اندازه 1 واحد افزایش می‌یابد.
طول عمر(lifetime) متغیرهای captured شده در حد طول عمر delegate افزایش پیدا می‌کند. در مثال زیر متغیر محلی seed در حالت معمول، محدوده دیدی (scope) در حد تعریف این متغیر تا پایان اجرای متد دارد. اما از آنجاییکه در اینجا متغیر captured شده است، طول عمر آن در حدا طول عمر delegate افزایش می‌یابد: theNatural
static Func<int> Natural()
{
 int seed = 0;
 return () => seed++; // Returns a closure
}
static void Main()
{
 Func<int> theNatural = Natural();
 Console.WriteLine (theNatural ()); // 0
 Console.WriteLine (theNatural ()); // 1
}
اگر متغیر seed را در بدنه عبارت لامبدا تعریف نماییم، این متغیر برای هر بار اجرای delegate یکتا خواهد بود:
static Func<int> Natural()
{
 return() => { int seed = 0; return seed++; };
}
static void Main()
{
 Func<int> natural = Natural();
 Console.WriteLine (natural()); // 0
 Console.WriteLine (natural()); // 0
}

نکته: پیاده سازی پروسه Capture شدن متغیر، به این صورت است که این متغیرها به عنوان یک فیلد از یک کلاس (با سطح دسترسی private) در نظر گرفته می‌شوند. زمانیکه متد فراخوانی شد، کلاس مزبور وهله سازی شده و طول عمر آن به  طول عمر delegate گره می‌خورد.

Capturing iteration variables
در حلقه for، وقتی که متغیر حلقه توسط یک عبارت لامبدا capture می‌گردد، #C با آن متغیر طوری رفتار می‌کند که گویی در خارج از حلقه تعریف شده‌است و این بدان معناست که در هر بار تکرار حلقه، مقدار یکسانی برای متغیر در نظر گرفته می‌شود. کد زیر 333 را در خروجی چاپ می‌کند(بجای 012). 
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
actions [i] = () => Console.Write (i);
foreach (Action a in actions) a(); // 333
دلیل این موضوع این است که در هنگام اجرای delegate ها، هر delegate مقدار i را برابر مقدار آن در زمان اجرا می‌بیند و این مقدار در زمان اجرا برابر با 3 می‌باشد.
با نوشتن کد زیر می‌توان درک بهتری از موضوع پیدا کرد. 
Action[] actions = new Action[3];
int i = 0;
actions[0] = () => Console.Write (i);
i = 1;
actions[1] = () => Console.Write (i);
i = 2;
actions[2] = () => Console.Write (i);
i = 3;
foreach (Action a in actions) a(); // 333
اگر بخواهیم خروجی 012 چاپ شود راه حل به شرح زیر خواهد بود:
Action[] actions = new Action[3];
for (int i = 0; i < 3; i++)
{
 int loopScopedi = i;
 actions [i] = () => Console.Write (loopScopedi);
}
foreach (Action a in actions) a(); // 012
زیرا هر متغیر loopScopedi در هر بار تکرار حلقه مجددا تعریف می‌گردد و لذا هر بار متغیر متفاوتی capture می‌گردد.