using System.Drawing; using System.Windows.Forms; namespace Navasser.Theme.VisualStudio { public class BlueMenuStrip:ToolStripProfessionalRenderer { protected override void OnRenderMenuItemBackground(ToolStripItemRenderEventArgs e) { var borderColor = ColorTranslator.FromHtml("#E5C365");//Menu Item Border var selectedMenuBackColor = ColorTranslator.FromHtml("#FDF4BF");//Menu Item Background var menuOpenedBackColor = ColorTranslator.FromHtml("#EAF0FF"); var borderPen = new Pen(borderColor); if (e.Item.Selected)//اگر آیتمی از منوها انتخاب شد { var selectedMenuBrush = new SolidBrush(selectedMenuBackColor); var selectedItemBounds = new Rectangle(Point.Empty, e.Item.Size);//اقدام به پر کردن یک مستطیل به اندازه ابعاد آیتم انتخاب شده میکند e.Graphics.FillRectangle(selectedMenuBrush,selectedItemBounds); e.Graphics.DrawRectangle(borderPen,0,0,selectedItemBounds.Width-1,selectedItemBounds.Height-1);// بوردر آیتم را رسم میکند e.Item.BackColor = menuOpenedBackColor; } else { base.OnRenderMenuItemBackground(e); e.ToolStrip.BackColor = ColorTranslator.FromHtml("#EAF0FF"); } } } }
using System.Drawing; using System.Windows.Forms; namespace Navasser.Theme.VisualStudio { public class BlueToolStrip : ToolStripProfessionalRenderer { protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { var toolStripBackColor = ColorTranslator.FromHtml("#D6DBE9"); var toolStripBrush = new SolidBrush(toolStripBackColor); e.Graphics.FillRectangle(toolStripBrush,0,0,e.AffectedBounds.Width + 10,e.AffectedBounds.Height); } protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) { var borderColor = ColorTranslator.FromHtml("#E5C365");//For border var selectedToolItemBackColor = ColorTranslator.FromHtml("#FDF4BF"); var selectedItemBrush = new SolidBrush(selectedToolItemBackColor); var pressedItemBackColor = ColorTranslator.FromHtml("#FFF29D"); var pressedItemBrush = new SolidBrush(pressedItemBackColor); var borderPen = new Pen(borderColor); if (e.Item.Selected) { e.Graphics.FillRectangle(selectedItemBrush,0,0,e.Item.Width,e.Item.Height); e.Graphics.DrawRectangle(borderPen,0,0,e.Item.Width-1,e.Item.Height-1); } if (e.Item.Pressed) { e.Graphics.FillRectangle(pressedItemBrush, 0, 0, e.Item.Width, e.Item.Height); e.Graphics.DrawRectangle(borderPen, 0, 0, e.Item.Width - 1, e.Item.Height - 1); } } } }
همچنین میتوانید برای انتخاب رنگهای دلخواه خودتان از ابزار ColorSchemer Studio استفاده کنید.
موجودیتهای زیر را در نظر بگیرید:
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 استفاده شود در زمان اجرا با خطای زیر مواجه خواهیم شد و در نتیجه در استفاده از این متدها به این مسئله باید دقت شود.
- <IDbSet<TEntity: که دارای متدهای Add, Attach, Create, Find, Remove, Local میباشد و برای بحث ما Find و Local جهت ساخت پرس و جو استفاده میشوند که در ادامه توضیح داده خواهند شد.
- <DbQuery<TEntity: که دارای متدهای AsNoTracking و Include میباشد و در ادامه توضیح داده خواهند شد.
- دادههای موجود در حافظه را بررسی میکند یعنی آنهایی که Load و یا Attach شده اند.
- داده هایی که به DbContext اضافه (Add) ولی هنوز در database درج نشده اند.
- داده هایی که در database هستند ولی هنوز Load نشده اند.
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); } }
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 } }
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"); } }
در این قسمت قصد داریم اطلاعات بازگشتی از لایه سرویس برنامه را کش کنیم؛ اما نمیخواهیم مدام کدهای مرتبط با کش کردن اطلاعات را در مکانهای مختلف لایه سرویس پراکنده کنیم. میخواهیم یک ویژگی یا Attribute سفارشی را تهیه کرده (مثلا به نام CacheMethod) و به متد یا متدهایی خاص اعمال کنیم. سپس برنامه، در زمان اجرا، بر اساس این ویژگیها، خروجیهای متدهای تزئین شده با ویژگی CacheMethod را کش کند.
در اینجا نیز از ترکیب StructureMap و DynamicProxy پروژه Castle، برای رسیدن به این مقصود استفاده خواهیم کرد. به کمک StructureMap میتوان در زمان وهله سازی کلاسها، آنها را به کمک متدی به نام EnrichWith توسط یک محصور کننده دلخواه، مزین یا غنی سازی کرد. این مزین کننده را جهت دخالت در فراخوانیهای متدها، یک DynamicProxy درنظر میگیریم. با پیاده سازی اینترفیس IInterceptor کتابخانه DynamicProxy مورد استفاده و تحت کنترل قرار دادن نحوه و زمان فراخوانی متدهای لایه سرویس، یکی از کارهایی را که میتوان انجام داد، کش کردن نتایج است که در ادامه به جزئیات آن خواهیم پرداخت.
پیشنیازها
ابتدا یک برنامه جدید کنسول را آغاز کنید. تنظیمات آنرا از حالت Client profile به Full تغییر دهید.
سپس همانند قسمتهای قبل، ارجاعات لازم را به StructureMap و Castle.Core نیز اضافه نمائید:
PM> Install-Package structuremap PM> Install-Package Castle.Core
از این جهت که از HttpRuntime.Cache قصد داریم استفاده کنیم. HttpRuntime.Cache در برنامههای کنسول نیز کار میکند. در این حالت از حافظه سیستم استفاده خواهد کرد و در پروژههای وب از کش IIS بهره میبرد.
ویژگی CacheMethod مورد استفاده
using System; namespace AOP02.Core { [AttributeUsage(AttributeTargets.Method)] public class CacheMethodAttribute : Attribute { public CacheMethodAttribute() { // مقدار پیش فرض SecondsToCache = 10; } public double SecondsToCache { get; set; } } }
در ویژگی CacheMethod، خاصیت SecondsToCache بیانگر مدت زمان کش شدن نتیجه متد خواهد بود.
ساختار لایه سرویس برنامه
using System; using System.Threading; using AOP02.Core; namespace AOP02.Services { public interface IMyService { string GetLongRunningResult(string input); } public class MyService : IMyService { [CacheMethod(SecondsToCache = 60)] public string GetLongRunningResult(string input) { Thread.Sleep(5000); // simulate a long running process return string.Format("Result of '{0}' returned at {1}", input, DateTime.Now); } } }
تدارک یک CacheInterceptor
using System; using System.Web; using Castle.DynamicProxy; namespace AOP02.Core { public class CacheInterceptor : IInterceptor { private static object lockObject = new object(); public void Intercept(IInvocation invocation) { cacheMethod(invocation); } private static void cacheMethod(IInvocation invocation) { var cacheMethodAttribute = getCacheMethodAttribute(invocation); if (cacheMethodAttribute == null) { // متد جاری توسط ویژگی کش شدن مزین نشده است // بنابراین آنرا اجرا کرده و کار را خاتمه میدهیم invocation.Proceed(); return; } // دراینجا مدت زمان کش شدن متد از ویژگی کش دریافت میشود var cacheDuration = ((CacheMethodAttribute)cacheMethodAttribute).SecondsToCache; // برای ذخیره سازی اطلاعات در کش نیاز است یک کلید منحصربفرد را // بر اساس نام متد و پارامترهای ارسالی به آن تهیه کنیم var cacheKey = getCacheKey(invocation); var cache = HttpRuntime.Cache; var cachedResult = cache.Get(cacheKey); if (cachedResult != null) { // اگر نتیجه بر اساس کلید تشکیل شده در کش موجود بود // همان را بازگشت میدهیم invocation.ReturnValue = cachedResult; } else { lock (lockObject) { // در غیر اینصورت ابتدا متد را اجرا کرده invocation.Proceed(); if (invocation.ReturnValue == null) return; // سپس نتیجه آنرا کش میکنیم cache.Insert(key: cacheKey, value: invocation.ReturnValue, dependencies: null, absoluteExpiration: DateTime.Now.AddSeconds(cacheDuration), slidingExpiration: TimeSpan.Zero); } } } private static Attribute getCacheMethodAttribute(IInvocation invocation) { var methodInfo = invocation.MethodInvocationTarget; if (methodInfo == null) { methodInfo = invocation.Method; } return Attribute.GetCustomAttribute(methodInfo, typeof(CacheMethodAttribute), true); } private static string getCacheKey(IInvocation invocation) { var cacheKey = invocation.Method.Name; foreach (var argument in invocation.Arguments) { cacheKey += ":" + argument; } // todo: بهتر است هش این کلید طولانی بازگشت داده شود // کار کردن با هش سریعتر خواهد بود return cacheKey; } } }
توضیحات ریز قسمتهای مختلف آن به صورت کامنت، جهت درک بهتر عملیات، ذکر شدهاند.
اتصال Interceptor به سیستم
خوب! تا اینجای کار صرفا تعاریف اولیه تدارک دیده شدهاند. در ادامه نیاز است تا DI و DynamicProxy را از وجود آنها مطلع کنیم.
using System; using AOP02.Core; using AOP02.Services; using Castle.DynamicProxy; using StructureMap; namespace AOP02 { class Program { static void Main(string[] args) { ObjectFactory.Initialize(x => { var dynamicProxy = new ProxyGenerator(); x.For<IMyService>() .EnrichAllWith(myTypeInterface => dynamicProxy.CreateInterfaceProxyWithTarget(myTypeInterface, new CacheInterceptor())) .Use<MyService>(); }); var myService = ObjectFactory.GetInstance<IMyService>(); Console.WriteLine(myService.GetLongRunningResult("Test")); Console.WriteLine(myService.GetLongRunningResult("Test")); } } }
حال اگر برنامه را اجرا کنید یک چنین خروجی قابل مشاهده خواهد بود:
Result of 'Test' returned at 2013/04/09 07:19:43 Result of 'Test' returned at 2013/04/09 07:19:43
از این پیاده سازی میشود به عنوان کش سطح دوم ORMها نیز استفاده کرد (صرفنظر از نوع ORM در حال استفاده).
دریافت مثال کامل این قسمت
AOP02.zip
نکاتی که باید جهت بررسی یکسان بودن دو URL بررسی کرد
using System; using System.Web; namespace UrlNormalizationTest { public static class UrlNormalization { public static bool AreTheSameUrls(this string url1, string url2) { url1 = url1.NormalizeUrl(); url2 = url2.NormalizeUrl(); return url1.Equals(url2); } public static bool AreTheSameUrls(this Uri uri1, Uri uri2) { var url1 = uri1.NormalizeUrl(); var url2 = uri2.NormalizeUrl(); return url1.Equals(url2); } public static string[] DefaultDirectoryIndexes = new[] { "default.asp", "default.aspx", "index.htm", "index.html", "index.php" }; public static string NormalizeUrl(this Uri uri) { var url = urlToLower(uri); url = limitProtocols(url); url = removeDefaultDirectoryIndexes(url); url = removeTheFragment(url); url = removeDuplicateSlashes(url); url = addWww(url); url = removeFeedburnerPart(url); return removeTrailingSlashAndEmptyQuery(url); } public static string NormalizeUrl(this string url) { return NormalizeUrl(new Uri(url)); } private static string removeFeedburnerPart(string url) { var idx = url.IndexOf("utm_source=", StringComparison.Ordinal); return idx == -1 ? url : url.Substring(0, idx - 1); } private static string addWww(string url) { if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www.")) { return url.Replace("://", "://www."); } return url; } private static string removeDuplicateSlashes(string url) { var path = new Uri(url).AbsolutePath; return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url; } private static string limitProtocols(string url) { return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url; } private static string removeTheFragment(string url) { var fragment = new Uri(url).Fragment; return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty); } private static string urlToLower(Uri uri) { return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant()); } private static string removeTrailingSlashAndEmptyQuery(string url) { return url .TrimEnd(new[] { '?' }) .TrimEnd(new[] { '/' }); } private static string removeDefaultDirectoryIndexes(string url) { foreach (var index in DefaultDirectoryIndexes) { if (url.EndsWith(index)) { url = url.TrimEnd(index.ToCharArray()); break; } } return url; } } }
using NUnit.Framework; using UrlNormalizationTest; namespace UrlNormalization.Tests { [TestFixture] public class UnitTests { [Test] public void Test1ConvertingTheSchemeAndHostToLowercase() { var url1 = "HTTP://www.Example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test2CapitalizingLettersInEscapeSequences() { var url1 = "http://www.example.com/a%c2%b1b".NormalizeUrl(); var url2 = "http://www.example.com/a%C2%B1b".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test3DecodingPercentEncodedOctetsOfUnreservedCharacters() { var url1 = "http://www.example.com/%7Eusername/".NormalizeUrl(); var url2 = "http://www.example.com/~username/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test4RemovingTheDefaultPort() { var url1 = "http://www.example.com:80/bar.html".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test5AddingTrailing() { var url1 = "http://www.example.com/alice".NormalizeUrl(); var url2 = "http://www.example.com/alice/?".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test6RemovingDotSegments() { var url1 = "http://www.example.com/../a/b/../c/./d.html".NormalizeUrl(); var url2 = "http://www.example.com/a/c/d.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex1() { var url1 = "http://www.example.com/default.asp".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex2() { var url1 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); var url2 = "http://www.example.com/default.asp?id=1".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test7RemovingDirectoryIndex3() { var url1 = "http://www.example.com/a/index.html".NormalizeUrl(); var url2 = "http://www.example.com/a/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test8RemovingTheFragment() { var url1 = "http://www.example.com/bar.html#section1".NormalizeUrl(); var url2 = "http://www.example.com/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test9LimitingProtocols() { var url1 = "https://www.example.com/".NormalizeUrl(); var url2 = "http://www.example.com/".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test10RemovingDuplicateSlashes() { var url1 = "http://www.example.com/foo//bar.html".NormalizeUrl(); var url2 = "http://www.example.com/foo/bar.html".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test11AddWww() { var url1 = "http://example.com/".NormalizeUrl(); var url2 = "http://www.example.com".NormalizeUrl(); Assert.AreEqual(url1, url2); } [Test] public void Test12RemoveFeedburnerPart() { var url1 = "http://site.net/2013/02/firefox-19-released/?utm_source=rss&utm_medium=rss&utm_campaign=firefox-19-released".NormalizeUrl(); var url2 = "http://site.net/2013/02/firefox-19-released".NormalizeUrl(); Assert.AreEqual(url1, url2); } } }
پروژه نمونه
تنظیمات ذیل را بر روی یک پروژه از نوع class library دات نت 4 در VS 2013 اعمال خواهیم کرد.
ویرایش فایل پروژه برنامه
برای اینکه تنظیمات کامپایل خودکار مخصوص دات نت 4.5 را نیز به این پروژه دات نت 4 اضافه کنیم، نیاز است فایل csproj آنرا مستقیما ویرایش نمائیم. این تغییرات شامل مراحل ذیل هستند:
الف) تعریف متغیر Framework
<PropertyGroup> <!-- ...--> <Framework Condition=" '$(Framework)' == '' ">NET40</Framework> </PropertyGroup>
ب) ویرایش مسیر خروجی تنظیمات کامپایل فعلی
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <!-- ...--> <OutputPath>bin\$(Configuration)\$(Framework)\</OutputPath> </PropertyGroup>
بدیهی است اگر حالت release هم وجود دارد، نیاز است مقدار OutputPath آنرا نیز به همین ترتیب ویرایش کرد.
ج) افزودن تنظیمات کامپایل دات نت 4.5 به پروژه جاری
<PropertyGroup Condition=" '$(Framework)' == 'NET45' And '$(Configuration)|$(Platform)' == 'Debug|AnyCPU'"> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <PlatformTarget>AnyCPU</PlatformTarget> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\$(Configuration)\$(Framework)\</OutputPath> <DefineConstants>DEBUG;TRACE;NET45</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> <PropertyGroup Condition=" '$(Framework)' == 'NET45' And '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <PlatformTarget>AnyCPU</PlatformTarget> <DebugType>pdbonly</DebugType> <Optimize>true</Optimize> <OutputPath>bin\$(Configuration)\$(Framework)\</OutputPath> <DefineConstants>TRACE;NET45</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup>
همچنین اگر به DefineConstants آن دقت کنید، مقدار NET45 نیز به آن اضافه شدهاست. این مورد سبب میشود که بتوانید در پروژهی جاری، شرطیهایی را ایجاد کنید که کدهای آن فقط در حین کامپایل برای دات نت 4.5 به خروجی اسمبلی نهایی اضافه شوند:
#if NET45 public class ExtensionAttribute : Attribute { } #endif
د) افزودن تنظیمات پس از build
در انتهای فایل csproj قسمت AfterBuild به صورت کامنت شده موجود است. آنرا به نحو ذیل تغییر دهید:
<Target Name="AfterBuild"> <Message Text="Enter After Build TargetFrameworkVersion:$(TargetFrameworkVersion) Framework:$(Framework)" Importance="high" /> <MSBuild Condition=" '$(Framework)' != 'NET45'" Projects="$(MSBuildProjectFile)" Properties="Framework=NET45" RunEachTargetSeparately="true" /> <Message Text="Exiting After Build TargetFrameworkVersion:$(TargetFrameworkVersion) Framework:$(Framework)" Importance="high" /> </Target>
برای آزمایش بیشتر، فایل csproj نهایی را از اینجا میتوانید دریافت کنید:
DualTargetFrameworks.zip
روش متداول ایجاد کارهای پس زمینه
سادهترین روش انجام کارهای پس زمینه در برنامههای دات نتی، استفاده از متدهایی هستند که یک ترد جدید را ایجاد میکنند مانند Task.Run, Task.Factory.StartNew, Delegate.BeginInvoke, ThreadPool.QueueUserWorkItem و امثال آن. اما ... این روشها در برنامههای ASP.NET ایدهی خوبی نیستند!
از این جهت که موتور ASP.NET در این حالات اصلا نمیداند که شما کار پس زمینهای را ایجاد کردهاید. به همین جهت اگر پروسهی برنامه پس از مدتی recycle شود، تمام کارهای پس زمینهی موجود نیز از بین خواهند رفت.
معرفی HostingEnvironment.QueueBackgroundWorkItem
متد HostingEnvironment.QueueBackgroundWorkItem به دات نت 4.5.2 اضافه شدهاست تا بتوان توسط آن یک کار پس زمینه را توسط موتور ASP.NET شروع کرد و نه مانند قبل، بدون اطلاع آن. البته باید دقت داشت که این کارهای پس زمینه مستقل از هر نوع درخواستی اجرا میشوند.
در این حالت چون موتور ASP.NET از وجود کار پس زمینهی آغاز شده مطلع است، در صورت فرا رسیدن زمان recycle شدن برنامه، کل AppDomain را به یکباره نابود نخواهد کرد. البته این مورد فقط به این معنا است که در صورت فرا رسیدن زمان recycle شدن پروسه، با تنظیم یک CancellationToken، اطلاع رسانی خواهد کرد. در این حالت حداکثر 30 ثانیه فرصت خواهید داشت تا کارهای پس زمینه را بدون مشکل خاتمه دهید. اگر کار پس زمینه در این مدت به پایان نرسد، همانند قبل، کل AppDomain نابود خواهد شد.
این متد دو overload دارد و در هر دو حالت، تنظیم خودکار پارامتر CancellationToken توسط ASP.NET، بیانگر آغاز زمان خاتمهی کل برنامه است:
public static void QueueBackgroundWorkItem(Action<CancellationToken> workItem); public static void QueueBackgroundWorkItem(Func<CancellationToken, Task> workItem);
علت استفاده از Action و Func در اینجا، امکان تعریف خلاصه و inline یک متد و ارسال پارامتری به آن از طرف برنامه است، بجای تعریف یک اینترفیس جدید، نیاز به پیاده سازی آن اینترفیس و بعد برای مثال ارسال یک مقدار از طرف برنامه به متد Stop آن (بجای تعریف یک اینترفیس تک متدی، از Action و یا Func نیز میتوان استفاده کرد).
نمونهای از نحوهی فراخوانی این دو overload را در ذیل مشاهده میکنید:
HostingEnvironment.QueueBackgroundWorkItem(cancellationToken => { //todo: ... }); HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => { //todo: ... await Task.Delay(20000, cancellationToken); });
پشت صحنهی HostingEnvironment.QueueBackgroundWorkItem
روش استاندارد ثبت و معرفی یک کار پس زمینه در ASP.NET، توسط پیاده سازی اینترفیسی به نام IRegisteredObject انجام میشود. سپس توسط متد HostingEnvironment.RegisterObject میتوان این کلاس را به موتور ASP.NET معرفی کرد. در این حالت زمانیکه AppDomain قرار است خاتمه یابد، متد Stop اینترفیس IRegisteredObject کار اطلاع رسانی را انجام میدهد. توسط QueueBackgroundWorkItem دقیقا از همین روش به همراه فراخوانی ThreadPool.QueueUserWorkItemجهت اجرای متد معرفی شدهی به آن استفاده میشود.
از مکانیزم IRegisteredObject در DNT Scheduler نیز استفاده شدهاست.
پیشنیازها
ابتدا نیاز است به خواص پروژه مراجعه کرده و Target framework را بر روی 5.4.2 قرار داد. اگر به روز رسانی دوم VS 2013 را نصب کرده باشید، این نگارش هم اکنون بر روی سیستم شما فعال است. اگر خیر، امکان دریافت و نصب آن، به صورت جداگانه نیز وجود دارد:
.NET Framework 4.5.2
Developer pack
محدودیتهای QueueBackgroundWorkItem
- از آن در خارج از یک برنامهی وب ASP.NET نمیتوان استفاده کرد.
- توسط آن، خاتمهی یک AppDomain تنها به مدت 30 ثانیه به تاخیر میافتد؛ تا فرصت داشته باشید کارهای در حال اجرا را با حداقل خسارت به پایان برسانید.
- یک work item، اطلاعاتی را از فراخوان خود دریافت نمیکند. به این معنا که مستقل از زمینهی یک درخواست اجرا میشود.
- استفادهی از آن الزاما به این معنا نیست که کار درخواستی شما حتما اجرا خواهد شد. زمانیکه که کار خاتمهی AppDomain آغاز میشود، فراخوانیهای QueueBackgroundWorkItem دیگر پردازش نخواهند شد.
- اگر برنامه به مقدار CancellationToken تنظیم شده توسط ASP.NET دقت نکند، جهت پایان یافتن کار در حال اجرا، صبر نخواهد شد.
در بسیاری از پروژههای دات نت، نیاز به استفاده از فایلهای نرم افزار آفیس، از قبیل ورد و اکسل و ... وجود دارد. برای مثال گاهی لازم است اطلاعات یک گرید، یا هر منبع دادهای، در قالب اکسل به کاربر نمایش داده شود. بدین شکل که این فایلها در زمان اجرا ساخته شده و به کاربر نمایش داده شود .حال فرض کنید شما روی سیستم خودتان Office2007 را نصب کرده اید و به اسمبلیهای این ورژن دسترسی دارید. البته بدون نیاز به نصب آفیس نیز میتوان به این توابع دسترسی داشت و از آنها در برنامه استفاده کرد که همان استفاده از Primary Interop Assemblies میباشد. مشکلی که ممکن است پیش آید این است که در کامپیوترهای کاربران ممکن است ورژنهای مختلفی از آفیس نصب باشد مانند 2003 -2007-2010-2013 و اگر با ورژن اسمبلیهایی که فراخوانیهای فایلهای اکسل از طریق آن انجام شده باشد متفاوت باشد، برنامه اجرا نمیشود .
در حالت معمول برای نمایش یک فایل آفیس مثل اکسل در برنامه، ابتدا اسمبلی مربوطه را (اکسل در این مثال) که به نام Microsoft.Office.Interop.Excel میباشد به اسمبلیهای برنامه اضافه کرده (از طریق add reference) و برای نمایش یک فایل اکسل در زمان اجرا از کدهای زیر استفاده مینماییم :
try { var application = (Microsoft.Office.Interop.Excel.Application)Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); Workbook wrkBook; var wbk = application.Workbooks; wrkBook = wbk.Add(); wrkBook.Activate(); application.Visible = true; } catch (Exception ex) { Error(ex.Message); }
حال اگر آفیس 2010 به عنوان مثال در سیستم ما نصب باشد، ورژن این اسمبلی 14 میباشد و اگر این برنامه را در کامپیوتر کلاینتی که آفیس 2007 بر روی آن نصب باشد انتشار دهیم اجرا نمیشود. برای حل این مشکل بنده با استفاده از روش dynamic این موضوع را حل کردم و بنظر میرسد راههای دیگری نیز برای حل آن وجود داشته باشد.
در این روش با توجه به ورژن آفیسی که بر روی سیستم کاربر نصب شده اسمبلی مربوطه را از سیستم کاربر لود کرده و فایلهای آفیس را اجرا مینماییم. در ابتدا تشخیص میدهیم چه ورژنی از آفیس بر روی سیستم کاربر نصب است :
string strVersion = null; dynamic objEApp =Activator.CreateInstance(Type.GetTypeFromProgID("Excel.Application")); if (objEApp.Version == "12.0") { strVersion = "2007"; } else if (objEApp.Version == "14.0") { strVersion = "2010"; }
روش دیگر برای انجام اینکار استفاده از اطلاعات رجیستری ویندوز است :
string strEVersionSubKey = "\\Excel.Application\\CurVer"; string strValue = null; //Value Present In Above Key string strVersion = null; //Determines Excel Version RegistryKey rkVersion = null; //Registry Key To Determine Excel Version rkVersion = Registry.ClassesRoot.OpenSubKey(strEVersionSubKey, false); //Open Registry Key if ((rkVersion != null)) //If Key Exists { strValue = (string)rkVersion.GetValue(string.Empty); //Get Value strValue = strValue.Substring(strValue.LastIndexOf(".") + 1); //Store Value switch (strValue) //Determine Version { case "11": strVersion = "2003"; break; case "12": strVersion = "2007"; break; case "14": strVersion = "2010"; break; }
if (strVersion == "2007") { string strAssemblyOff2007 = "Microsoft.Office.Interop.Excel, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"; try { Assembly xslExcelAssembly = Assembly.Load(strAssemblyOff2007); //Load Assembly Type type = xslExcelAssembly.GetTypes().Single(t => t.Name == "ApplicationClass"); dynamic application = Activator.CreateInstance(type); var workbooks = application.Workbooks; var workbook = workbooks.Add(); var worksheet = workbook.Worksheets[1]; workbook.Activate(); application.Visible = true; } catch (Exception ex) { } }
<sfListView:SfListView.ItemTemplate> <DataTemplate> <FlexLayout x:DataType="model:Product" CompressedLayout.IsHeadless="True" Direction="Row"> <Label FlexLayout.Basis="40%" Text="{Binding Name}" VerticalTextAlignment="Center" /> ...
<ffimageloading:CachedImage CacheType="All" DownsampleToViewSize="true" HeightRequest="100" HorizontalOptions="Center" Source="https://avatars2.githubusercontent.com/u/22663390?s=100" VerticalOptions="Center" WidthRequest="100" />
CoffeeScript #13
بخشهای بد
در ادامهی قسمت قبل، به مواردی که توسط CoffeeScript اصلاح شدهاند، میپردازیم.
Reserved words
کلمات کلیدی خاصی در جاوااسکریپت وجود دارد مانند class، enum و const که برای نسخههای بعدی جاوااسکریپت در آینده رزرو شدهاند. استفاده از این کلمات در برنامههای جاوااسکریپت میتواند نتایج غیرقابل پیش بینی داشته باشد. برخی از مرورگرهای به خوبی از عهدهی این کار برمیآیند و بعضی دیگر به طور کامل جلوی استفاده از اینها را گرفتهاند. CoffeeScript بعد از تشخیص استفاده از یک کلمهی کلیدی، با یک راه کار خاص، از این موضوع میگریزد.
به عنوان مثال، فرض کنید میخواهیم از کلمه کلیدی class به عنوان یک خصوصیت در یک شیء استفاده کنیم:
myObj = { delete: "I am a keyword!" } myObj.class = ->
var myObj; myObj = { "delete": "I am a keyword!" }; myObj["class"] = function() {};
Equality comparisons
مقایسه برابری ضعف دیگری است که در جاوااسکریپت باعث ایجاد رفتاری گیج کننده و اغلب باعث ایجاد اشکالاتی در کد نوشته شده میشود. به مثال زیر توجه کنید:
"" == "0"// false 0 == ""// true 0 == "0"// true false == "false"// false false == "0"// true false == undefined// false false == null// false null == undefined// true " \t\r\n" == 0// true
راه حل این کار استفاده از عملگر برابری سختگیرانه است، که از 3 مساوی تشکیل شده است: === عملگر برابر سخت گیرانه دقیقا مانند عملگر برابری عادی عمل میکند و تنها نوع دادهها را بررسی میکند که با هم برابر باشند.
توصیه میشود که همیشه از عملگر برابری سختگیرانه استفاده کنید و هرجا لازم بود قبل مقایسه عمل تبدیل نوع دادهها را انجام دهید.
CoffeeScript این مشکل را به صورت کامل حل کرده است؛ یعنی هر جایی که عمل مقایسه == انجام شود به === تبدیل میشود. شما باید به صورت صریح نوع دادهها را قبل از مقایسه تبدیل کرده باشید.
نکته: در مقایسهها رشته خالی ""، null ،undefined و عدد 0 همگی false برمی گردانند.
alert "Empty Array" unless [].length alert "Empty String" unless "" alert "Number 0" unless 0
if (![].length) { alert("Empty Array"); } if (!"") { alert("Empty String"); } if (!0) { alert("Number 0"); }
alert "This is not called" unless ""?
if ("" == null) { alert("This is not called"); }
Function definition
خیلی جالب است که در جاوااسکریپت میتوانید تابعی را بعد از اینکه فراخوانی کردید، تعریف کنید. به عنوان مثال، کد زیر به صورت کامل اجرا میشود:
wem(); function wem() {alert("hi");}
if (true) { function declaration() { return "first"; } } else { function declaration() { return "second"; } } declaration();
، تابع ()declaration مقدار "first" را برگشت خواهد داد و در دیگر مرورگرها مانند Chrome، مقدار "second" برگشت داده خواهد شد. در حالیکه به نظر میرسد که قسمت else هیچگاه اجرا نخواهد شد.در صورتیکه علاقمند به کسب اطلاعات بیشتری دربارهی نحوه تعریف توابع، هستید باید راهنمای آقای Juriy Zaytsev را مطالعه کنید. به صورت خلاصه، رفتار نسبتا مبهم مرورگرها میتواند منجر به ایجاد مشکلاتی در مسیر نوشتن یک پروژه شوند.
همه چیز در CoffeeScript در نظر گرفته شده است و بهترین روش برای حل این مشکل، حذف کلمه function و به جای آن استفاده از عبارت (expression) تابع است.
Number property lookups
نقصی که در پارسر جاوااسکریپت در مواجه با نماد نقطه (dot notation) بر روی اعداد وجود دارد، تفسیر آن به ممیز شناور، بجای مراجعه به ویژگیهای آن است. برای مثال کد جاوااسکریپت زیر باعث ایجاد خطای نحوی میشود:5.toString();
(5).toString(); 5..toString();