با کمک امکانات ارائه شده توسط LINQ ، میتوان بسیاری از اعمال برنامه نویسی را در حجمی کمتر، خواناتر و در نتیجه با قابلیت نگهداری بهتر، انجام داد که تعدادی از آنها را در ادامه مرور خواهیم کرد.
الف) تهیه یک یک رشته، حاوی عناصر یک آرایه، جدا شده با کاما.
using System.Linq;
public class CLinq
{
public static string GetCommaSeparatedListNormal(string[] data)
{
string items = string.Empty;
foreach (var item in data)
{
items += item + ", ";
}
return items.Remove(items.Length - 2, 1).Trim();
}
public static string GetCommaSeparatedList(string[] data)
{
return data.Aggregate((s1, s2) => s1 + ", " + s2);
}
}
ب) پیدا کردن تعداد عناصر یک آرایه حاوی مقداری مشخص
برای مثال آرایه زیر را در نظر بگیرید:
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
در تابع GetCountNormal زیر، این کار به شکلی متداول انجام شده و در GetCount از LINQ Count extension method کمک گرفته شده است.
using System.Linq;
public class CLinq
{
public static int GetCountNormal()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
var count = 0;
foreach (var name in names)
{
if (name.Contains("name"))
count += 1;
}
return count;
}
public static int GetCount()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
return names.Count(name => name.Contains("name"));
}
}
ج) دریافت لیستی از عناصر شروع شده با یک عبارت
در اینجا نیز دو روش متداول و استفاده از LINQ بررسی شده است.
using System.Linq;
using System.Collections.Generic;
public class CLinq
{
public static List<string> GetListNormal()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
List<string> result = new List<string>();
foreach (var item in sampleList)
{
if (item.StartsWith("P"))
result.Add(item);
}
return result;
}
public static List<string> GetList()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
return sampleList.Where(x => x.StartsWith("P")).ToList();
}
}
و در حالت کلی، اکثر حلقههای foreach متداول را میتوان با نمونههای خواناتر کوئریهای LINQ معادل، جایگزین کرد.
عموما در ORMها دو سطح کش میتواند وجود داشته باشد:
الف) سطح اول کش
که نمونه بارز آن در EF Code first استفاده از متد context.Entity.Find است. در بار اول فراخوانی این متد، مراجعهای به بانک اطلاعاتی صورت گرفته تا بر اساس primary key ذکر شده در آرگومان آن، رکورد متناظری بازگشت داده شود. در بار دوم فراخوانی متد Find، دیگر مراجعهای به بانک اطلاعاتی صورت نخواهد گرفت و اطلاعات از سطح اول کش (یا همان Context جاری) خوانده میشود.
بنابراین سطح اول کش در طول عمر یک تراکنش معنا پیدا میکند و به صورت خودکار توسط EF مدیریت میشود.
ب) سطح دوم کش
سطح دوم کش در ORMها طول عمر بیشتری داشته و سراسری است. هدف از آن کش کردن اطلاعات عمومی و پر مصرفی است که در دید تمام کاربران قرار دارد و همچنین تمام کاربران میتوانند به آن دسترسی داشته باشند. بنابراین محدود به یک Context نیست.
عموما پیاده سازی سطح دوم کش خارج از ORM مورد استفاده قرار میگیرد و توسط اشخاص و شرکتهای ثالث تهیه میشود.
در حال حاضر پیاده سازی توکاری از سطح دوم کش در EF Code first وجود ندارد و قصد داریم در مطلب جاری به یک پیاده سازی نسبتا خوب از آن برسیم.
تلاشهای صورت گرفته
تا کنون دو پیاده سازی نسبتا خوب از سطح دوم کش در EF صورت گرفته:
Entity Framework Code First Caching
Caching the results of LINQ queries
مورد اول برای ایده گرفتن خوب است. بحث اصلی پیاده سازی سطح دوم کش، یافتن کلیدی است که معادل کوئری LINQ در حال فراخوانی است. سطح دوم کش را به صورت یک Dictionary تصور کنید. هر آیتم آن تشکیل شده است از یک کلید و یک مقدار. از کلید برای یافتن مقدار متناظر استفاده میشود.
اکنون مشکل چیست؟ در یک برنامه ممکن است صدها کوئری لینک وجود داشته باشد. چطور باید به ازای هر کوئری LINQ یک کلید منحصربفرد تولید کرد؟
در مطلب «Entity Framework Code First Caching» از متد ToString استفاده شده است. اگر این متد، بر روی یک عبارت LINQ در EF Code first فراخوانی شود، معادل SQL آن نمایش داده میشود. بنابراین یک قدم به تولید کلید منحصربفرد متناظر با یک کوئری نزدیک شدهایم. اما ... مشکل اینجا است که متد ToString پارامترها را لحاظ نمیکند. بنابراین این روش اصلا قابل استفاده نیست. چون کاربر به ازای تمام پارامترهای ارسالی، همواره یک نتیجه را دریافت خواهد کرد.
در مقاله «Caching the results of LINQ queries» این مشکل برطرف شده است. با parse کامل expression tree یک عبارت LINQ کلید منحصربفرد معادل آن یافت میشود. سپس بر این اساس میتوان نتیجه کوئری را به نحو صحیحی کش کرد. در این روش پارامترها هم لحاظ میشوند و مشکل مقاله قبلی را ندارد.
اما این مقاله دوم یک مشکل مهم را به همراه دارد: روشی را برای حذف آیتمها از کش ارائه نمیدهد. فرض کنید مقالات سایت را در سطح دوم کش قرار دادهاید. اکنون یک مقاله جدید در سایت ثبت شده است. اصطلاحا برای invalidating کش در این روش، راهکاری پیشنهاد نشده است.
پیاده سازی بهتری از سطح دوم کش در EF Code fist
میتوان از همان روش یافتن کلید منحصربفرد معادل با یک کوئری LINQ، که در مقاله دوم فوق، یاد شد، کار را شروع کرد و سپس آنرا به مرحلهای رساند که مباحث حذف کش نیز به صورت خودکار مدیریت شود. پیاده سازی آن را برای برنامههای وب در ذیل ملاحظه میکنید:
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Objects; using System.Diagnostics; using System.Linq; using System.Web; using System.Web.Caching; namespace EfSecondLevelCaching.Core { public static class EfHttpRuntimeCacheProvider { #region Methods (6) // Public Methods (2) public static IList<TEntity> ToCacheableList<TEntity>( this IQueryable<TEntity> query, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { return query.Cacheable(x => x.ToList(), durationMinutes, priority); } /// <summary> /// Returns the result of the query; if possible from the cache, otherwise /// the query is materialized and the result cached before being returned. /// The cache entry has a one minute sliding expiration with normal priority. /// </summary> public static TResult Cacheable<TEntity, TResult>( this IQueryable<TEntity> query, Func<IQueryable<TEntity>, TResult> materializer, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { // Gets a cache key for a query. var queryCacheKey = query.GetCacheKey(); // The name of the cache key used to clear the cache. All cached items depend on this key. var rootCacheKey = typeof(TEntity).FullName; // Try to get the query result from the cache. printAllCachedKeys(); var result = HttpRuntime.Cache.Get(queryCacheKey); if (result != null) { debugWriteLine("Fetching object '{0}__{1}' from the cache.", rootCacheKey, queryCacheKey); return (TResult)result; } // Materialize the query. result = materializer(query); // Adding new data. debugWriteLine("Adding new data: queryKey={0}, dependencyKey={1}", queryCacheKey, rootCacheKey); storeRootCacheKey(rootCacheKey); HttpRuntime.Cache.Insert( key: queryCacheKey, value: result, dependencies: new CacheDependency(null, new[] { rootCacheKey }), absoluteExpiration: DateTime.Now.AddMinutes(durationMinutes), slidingExpiration: Cache.NoSlidingExpiration, priority: priority, onRemoveCallback: null); return (TResult)result; } /// <summary> /// Call this method in `public override int SaveChanges()` of your DbContext class /// to Invalidate Second Level Cache automatically. /// </summary> public static void InvalidateSecondLevelCache(this DbContext ctx) { var changedEntityNames = ctx.ChangeTracker .Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToList(); if (!changedEntityNames.Any()) return; printAllCachedKeys(); foreach (var item in changedEntityNames) { item.removeEntityCache(); } printAllCachedKeys(); } // Private Methods (4) private static void debugWriteLine(string format, params object[] args) { if (!Debugger.IsAttached) return; Debug.WriteLine(format, args); } private static void printAllCachedKeys() { if (!Debugger.IsAttached) return; debugWriteLine("Available cached keys list:"); int count = 0; var enumerator = HttpRuntime.Cache.GetEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Key.ToString().StartsWith("__")) continue; // such as __System.Web.WebPages.Deployment debugWriteLine("queryKey: {0}", enumerator.Key.ToString()); count++; } debugWriteLine("count: {0}", count); } private static void removeEntityCache(this string rootCacheKey) { if (string.IsNullOrWhiteSpace(rootCacheKey)) return; debugWriteLine("Removing items with dependencyKey={0}", rootCacheKey); // Removes all cached items depend on this key. HttpRuntime.Cache.Remove(rootCacheKey); } private static void storeRootCacheKey(string rootCacheKey) { // The cacheKeys of a cacheDependency that are not already in cache ARE NOT inserted into the cache // on the Insert of the item in which the dependency is used. if (HttpRuntime.Cache.Get(rootCacheKey) != null) return; HttpRuntime.Cache.Add( rootCacheKey, rootCacheKey, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } #endregion Methods } }
توضیحات کدهای فوق
در اینجا یک متدالحاقی به نام Cacheable توسعه داده شده است که میتواند در انتهای کوئریهای LINQ شما قرار گیرد. مثلا:
var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
کاری که در این متد انجام میشود به این شرح است:
الف) ابتدا کلید منحصربفرد معادل کوئری LINQ فراخوانی شده محاسبه میشود.
ب) بر اساس نام کامل نوع Entity در حال استفاده، کلید دیگری به نام rootCacheKey تولید میگردد.
شاید بپرسید اهمیت این کلید چیست؟
فرض کنید در حال حاضر 1000 آیتم در کش وجود دارند. چه روشی را برای حذف آیتمهای مرتبط با کش Entity1 پیشنهاد میدهید؟ احتمالا خواهید گفت تمام کش را بررسی کرده و آیتمها را یکی یکی حذف میکنیم.
این روش بسیار کند است (و جواب هم نمیدهد؛ چون کلیدی که در اینجا تولید شده، هش MD5 معادل کوئری است و نمیتوان آنرا به موجودیتی خاص ربط داد) و ... نکته جالبی در متد HttpRuntime.Cache.Insert برای مدیریت آن پیش بینی شده است: استفاده از CacheDependency.
توسط CacheDependency میتوان گروهی از آیتمهای همخانواده را تشکیل داد. سپس برای حذف کل این گروه کافی است کلید اصلی CacheDependency را حذف کرد. به این ترتیب به صورت خودکار کل کش مرتبط خالی میشود.
ج) مراحل بعدی آن هم یک سری اعمال متداول هستند. ابتدا توسط HttpRuntime.Cache.Get بررسی میشود که آیا بر اساس کلید متناظر با کوئری جاری، اطلاعاتی در کش وجود دارد یا خیر. اگر بله، نتیجه از کش خوانده میشود. اگر خیر، کوئری اصطلاحا materialized میشود تا بر روی بانک اطلاعاتی اجرا شده و نتیجه بازگشت داده شود. سپس این نتیجه را در کش قرار میدهیم.
مورد بعدی که باید به آن دقت داشت، خالی کردن کش، پس از به روز رسانی اطلاعات توسط کاربران است. این کار در متد InvalidateSecondLevelCache صورت میگیرد. به کمک ChangeTracker میتوان نام نوعهای موجودیتهای تغییر کرده را یافت. چون کلید اصلی CacheDependency را بر مبنای همین نام نوعهای موجودیتها تعیین کردهایم، به سادگی میتوان کش مرتبط با موجودیت یافت شده را خالی کرد.
استفاده از متد InvalidateSecondLevelCache یاد شده به نحو زیر است:
using System.Data.Entity; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.Models; namespace EfSecondLevelCaching.Test.DataLayer { public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public override int SaveChanges() { this.InvalidateSecondLevelCache(); return base.SaveChanges(); } } }
در اینجا با تحریف متد SaveChanges، میتوان درست در زمان اعمال تغییرات به بانک اطلاعاتی، قسمتی از کش را غیرمعتبر کرد.
نحوه استفاده از سطح دوم کش توسعه داده شده
مثالی از کاربرد متدهای الحاقی توسعه داده شده را در ذیل مشاهده میکنید:
using System.Data.Entity; using System.Linq; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.DataLayer; using EfSecondLevelCaching.Test.Models; using System; namespace EfSecondLevelCaching { public static class TestUsages { public static void RunQueries() { using (ProductContext context = new ProductContext()) { var isActive = true; var name = "Product1"; // reading from db var list1 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list2 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list3 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from db var list4 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == "Product2") .ToCacheableList(); } // removes products cache using (ProductContext context = new ProductContext()) { var p = new Product() { IsActive = false, ProductName = "P4", ProductNumber = "004" }; context.Products.Add(p); context.SaveChanges(); } using (ProductContext context = new ProductContext()) { var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); var data2 = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); context.SaveChanges(); } } } }
در این حالت اگر برنامه را اجرا کنیم به یک چنین خروجی در پنجره Debug ویژوال استودیو خواهیم رسید:
Adding new data: queryKey=72AF5DA1BA9B91E24DCCF83E88AD1C5F, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Adding new data: queryKey=11A2C33F9AD7821A0A31003BFF1DF886, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F queryKey: 11A2C33F9AD7821A0A31003BFF1DF886 queryKey: EfSecondLevelCaching.Test.Models.Product count: 3 Removing items with dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: count: 0 Available cached keys list: count: 0 Adding new data: queryKey=02E6FE403B461E45C5508684156C1D10, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 02E6FE403B461E45C5508684156C1D10 queryKey: EfSecondLevelCaching.Test.Models.Product count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__02E6FE403B461E45C5508684156C1D10' from the cache.
توضیحات:
در زمان تولید list1 چون اطلاعاتی در کش سطح دوم وجود ندارد، پیغام Adding new data قابل مشاهده است. اطلاعات از بانک اطلاعاتی دریافت شده و سپس در کش قرار داده میشود.
حین فراخوانی list2 که دقیقا همان کوئری list1 را یکبار دیگر فراخوانی میکند، به عبارت Fetching object خواهیم رسید که بر دریافت اطلاعات از کش سطح دوم بجای مراجعه به بانک اطلاعاتی دلالت دارد.
در list4 چون پارامترهای کوئری تغییر کردهاند، بنابراین دیگر کلید منحصربفرد معادل آن با list1 و lis2 یکی نیست و اینبار پیغام Adding new data مشاهده میشود؛ چون برای دریافت اطلاعات آن نیاز است که به بانک اطلاعاتی مراجعه شود.
در ادامه یک context دیگر باز شده و در آن رکوردی به بانک اطلاعاتی اضافه میشود. به همین دلیل اینبار پیام Removing items with dependencyKey قابل مشاهده است. به عبارتی متد InvalidateSecondLevelCache وارد عمل شده است و بر اساس تغییری که صورت گرفته، کش را غیرمعتبر کرده است.
سپس در context بعدی تعریف شده، دوبار متد FirstOrDefault فراخوانی شده است. اولین مورد Adding new data است و دومین فراخوانی به Fetching object ختم شده است (دریافت اطلاعات از کش).
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید:
EfSecondLevelCaching.zip
مقدمات راهبری (Navigation) در سیلورلایت را در اینجا میتوانید مطالعه نمائید : +
مطلبی را که در فصل فوق نخواهید یافت در مورد نحوهی بکارگیری الگوی MVVM جهت پیاده سازی Navigation در یک برنامهی سیلورلایت است؛ علت آن هم به این بر میگردد که این فصل پیش از مباحث Binding مطرح شد.
صورت مساله:
یکی از اصول MVVM این است که در ViewModel نباید ارجاعی از View وجود داشته باشد (ViewModel باید در بیخبری کامل از وجود اشیاء UI و ارجاع مستقیم به آنها طراحی شود)، اما برای پیاده سازی مباحث Navigation نیاز است به نحوی به شیء Frame قرار داده شده در صفحهی اصلی یا قالب اصلی برنامه دسترسی یافت تا بتوان درخواست رهنمون شدن به صفحات مختلف را صادر کرد. اکنون چکار باید کرد؟
راه حل:
یکی از راه حلهای جالبی که برای این منظور وجود دارد استفاده از امکانات کلاس Messenger مجموعهی MVVM Light toolkit است. از طریق ViewModel برنامه، آدرس صفحهی مورد نظر را به صورت یک پیغام به View مورد نظر ارسال میکنیم و سپس View برنامه که به این پیغامها گوش فرا میدهد، پس از دریافت آدرس مورد نظر، نسبت به فراخوانی تابع Navigate شیء Frame رابط کاربری برنامه اقدام خواهد کرد. به این صورت ViewModel برنامه به View خود جهت اعمال راهبری برنامه، گره نخواهد خورد.
روش پیاده سازی:
ابتدا ساختار پروژه را در نظر بگیرید (این شکل دگرگون شدهی Solution explorer مرتبط است با productivity tools نصب شده):
در پوشهی Views ، دو صفحه اضافه شدهاند که توسط user control ایی به نام menu لیست شده و راهبری خواهند شد. مونتاژ نهایی هم در MainPage.xaml صورت میگیرد.
کدهای XAML مرتبط با منوی ساده برنامه به شرح زیر هستند (Menu.xaml) :
<UserControl x:Class="MvvmLight6.Views.Menu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:MvvmLight6.ViewModels" mc:Ignorable="d"
FlowDirection="RightToLeft" d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<vm:MenuViewModel x:Key="vmMenuViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMenuViewModel}}">
<HyperlinkButton Content="صفحه یک" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page1.xaml"
/>
<HyperlinkButton Content="صفحه دو" Margin="5"
Command="{Binding DoNavigate}"
CommandParameter="/Views/Page2.xaml"
/>
</StackPanel>
</UserControl>
کدهای ViewModel مرتبط با این View که کار Command گردانی را انجام خواهد داد به شرح زیر است:
using GalaSoft.MvvmLight.Command;
using GalaSoft.MvvmLight.Messaging;
namespace MvvmLight6.ViewModels
{
public class MenuViewModel
{
public RelayCommand<string> DoNavigate { set; get; }
public MenuViewModel()
{
DoNavigate = new RelayCommand<string>(doNavigate);
}
private static void doNavigate(string url)
{
Messenger.Default.Send(url, "MyNavigationService");
}
}
}
تمام آیتمهای منوی فوق یک روال را صدا خواهند زد : DoNavigate . تنها تفاوت آنها در CommandParameter ارسالی به RelayCommand ما است که حاوی آدرس قرارگیری فایلهای صفحات تعریف شده است. این آدرسها با کمک امکانات کلاس Messenger مجموعهی MVVM light toolkit به View اصلی برنامه ارسال میگردند.
کدهای XAML مرتبط با MainPage.xaml به شرح زیر هستند:
<UserControl x:Class="MvvmLight6.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdk="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
xmlns:usr="clr-namespace:MvvmLight6.Views"
mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="400">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="268" />
</Grid.ColumnDefinitions>
<usr:Menu Grid.Column="1" />
<sdk:Frame Margin="5"
Name="frame1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Grid.Column="0" />
</Grid>
</UserControl>
و کار دریافت پیغامها (یا همان آدرس صفحات جهت انجام راهبری) و عکس العمل نشان دادن به آنها توسط کدهای ذیل صورت خواهد گرفت:
using System;
using GalaSoft.MvvmLight.Messaging;
namespace MvvmLight6
{
public partial class MainPage
{
public MainPage()
{
registerMessenger();
InitializeComponent();
}
private void registerMessenger()
{
Messenger.Default.Register<string>(this, "MyNavigationService", doNavigate);
}
private void doNavigate(string uri)
{
frame1.Navigate(new Uri(uri, UriKind.Relative));
}
}
}
ابتدا یک Messenger در اینجا رجیستر میشود و سپس به ازای هر بار دریافت پیغامی با token مساوی MyNavigationService ، متد doNavigate فراخوانی خواهد گردید.
کدهای این مثال را از اینجا میتوانید دریافت کنید.
در ASP.NET MVC آیا ViewModel همان DTO است؟
اگر با MVVM آشنایی داشته باشید حتماً متوجه شباهت زیاد ViewModel با DTO شده اید.
البته میدانیم که ViewModel در WPF شامل رفتار اشیاء هم میتواند باشد اما DTO به هیچ عنوان دربرگیرنده رفتار اشیاء نیست.به نظرم DTO بیشتر از اینکه به ViewModel شبیه باشد به Model در MVVM شبیه است.
Model در MVVM را میتواند معادلی برای ViewModel در MVC قرار داد و ViewModel در MVVM را میتوان شبیه Controller در MVC در نظر گرفت.
به نظر من تفاوتی که DTO با ViewModel دارد این است =>
کلاس StudentService نیاز به اطلاعات موجودیت Teacher از جمله نام و شماره شناسنامه دارد . در نتیجه باید متدی در TeacherService فراهم شود که این اطلاعات را تامین کند :
public class TeacherService { internal IEnumerable<TeacherDto> GetTeachers () { return ... } }
چون متد بالا قرار نیست اطلاعاتی را به لایهی نمایش پاس دهد و اطلاعات مستقیما در یک متد سرویس استفاده میشود خروجی آن Dto تعیین شده.
به نظر من این قواعد وحی منزل نیست ، به عنوان مثال شاید شخصی در طراحی خود از DTOها به عنوان اعضای ViewModelها استفاده کند یا DTO را به Presentation layer برگرداند و آن را به ViewModel نگاشت کند.
اما در نهایت پاسخ من به این نظرسنجی گزینهی خیر است.
عملگرهای پرس و جوی تبدیل، توالیهایی را که از جنس <IEnumerable<T هستند، به انواع دیگر مجموعه تبدیل میکنند.
از عملگرهای پرس و جوی زیر میتوان برای تبدیل توالیها استفاده کرد :
- OfType
- Cast
- ToArray
- ToList
- ToDictionary
- ToLookup
عملگر OfType
این عملگر عناصری از توالی را که نوع آنها را مشخص میکنیم باز میگرداند.
امضاء عملگر پرس و جوی OfType به صورت زیر است :
public static IEnumerable<TResult> OfType<TResult>(this IEnumerable source)
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime()}; IEnumerable<string> query = input.OfType<string>(); foreach (var item in query) { Console.WriteLine(item); }
Apple Sugar
مثال :کد زیر یک ساختار سلسله مراتبی شیء گرا را نمایش میدهد:
class Ingredient { public string Name { get; set; } } class DryIngredient : Ingredient { public int Grams { get; set; } } class WetIngredient : Ingredient { public int Millilitres { get; set; } }
IEnumerable<Ingredient> input = new Ingredient[] { new DryIngredient { Name = "Flour" }, new WetIngredient { Name = "Milk" }, new WetIngredient { Name = "Water" } }; IEnumerable<WetIngredient> query = input.OfType<WetIngredient>(); foreach (WetIngredient item in query) { Console.WriteLine(item.Name); }
Milk Water
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Cast
عملگر Cast همانند عملگر OfType رفتار میکند. این عملگر یک توالی ورودی را دریافت و بر اساس نوع مشخص شده، توالی خروجی را تولید میکند. همهی عناصر توالی ورودی به نوع مشخص شده Cast میشوند. اما بر عکس عملگر OfType که عناصری را که با نوع دادهی ما سازگاری نداشت، نادیده میگرفت، این عملگر در صورت عدم موفقیت در عملیات تغییر نقش (Cast)، یک استثناء را پرتاب میکند.
IEnumerable input = new object[] { "Apple", 33, "Sugar", 44, 'a', new DateTime() }; IEnumerable<string> query = input.Cast<string>(); foreach (string item in query) { Console.WriteLine(item); }
Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
پیاده سازی توسط عبارتهای جستجو
کلمهی کلیدی جایگزینی برای عملگر Cast، در عبارتهای جستجو وجود ندارد.این عملگر با استفاده از متغیر Range که در مطالب قبلی این سری معرفی شد، قابل پیاده سازی میباشد.
IEnumerable input = new object[]{ "Apple", "Sugar", "Flour" }; IEnumerable<string> query = from string i in input select i; foreach (var item in query) { Console.WriteLine(item); }
عملگر ToArray
عملگر ToArray یک توالی ورودی را دریافت و یک توالی خروجی را به صورت آرایه تولید میکند. این عملگر باعث اجرای سریع پرس و جو میشود و رفتار پیش فرض LINQ را که اجرای با تاخیر میباشد، تحریف/بازنویسی (Override) میکند.
مثال: در این مثال یک توالی از نوع <IEnumerable<string به یک آرایه رشتهای تبدیل شده است (تبدیل لیست به آرایه).
IEnumerable<string> input = new List<string> { "Apple", "Sugar", "Flour" }; string[] array = input.ToArray();
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToList
عملگر ToList همچون ToArray، اجرای با تاخیر را نادیده میگیرد. عملگر ToList همانطور که از نامش پیداست، توالی خروجی را بهصورت لیست مهیا میکند.
مثال:
IEnumerable<string> input = new[] { "Apple", "Sugar", "Flour" }; List<string> list = input.ToList();
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToDictionary
این عملگر توالی ورودی را به یک دیکشنری جنریک تبدیل میکند (<Dictinary<TKey,TValue) .
سادهترین امضاء عملگر ToDictionary، یک عبارت Lambda میباشد. این عبارت Lambda نشان دهندهی یک تابع است که عنصر کلید(Key) را در دیکشنری، مشخص میکند.
مثال:
class Recipe { public int Id { get; set; } public string Name { get; set; } public int Rating { get; set; } } IEnumerable<Recipe> recipes = new[] { new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 }, new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 }, new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 } }; Dictionary<int, Recipe> dict = recipes.ToDictionary(x => x.Id); foreach (KeyValuePair<int, Recipe> item in dict) { Console.WriteLine($"Key={item.Key}, Recipe={item.Value}"); }
خروجی مثال بالا:
Key=1, Recipe=Apple Pie Key=2, Recipe=Cherry Pie Key=3, Recipe=Beef Pie
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ToLookup
این عملگر رفتاری شبیه به عملگر ToDictionary را دارد، اما به جای تولید خروجی از نوع دیکشنری، نمونهای از جنس ILookUp را ایجاد میکند.
در کد زیر خروجی ایجاد شده توسط lookup دستورالعملها (Recipes) را بر حسب امتیاز آنها گروه بندی کرده است. در این مثال کلید، بر حسب Byte میباشد.
مثال :
class Recipe { public int Id { get; set; } public string Name { get; set; } public byte Rating { get; set; } } IEnumerable<Recipe> recipes = new[] { new Recipe { Id = 1, Name = "Apple Pie", Rating = 5 }, new Recipe { Id = 1, Name = "Banana Pie", Rating = 5 }, new Recipe { Id = 2, Name = "Cherry Pie", Rating = 2 }, new Recipe { Id = 3, Name = "Beef Pie", Rating = 3 } }; ILookup<byte, Recipe> look = recipes.ToLookup(x => x.Rating); foreach (IGrouping<byte, Recipe> ratingGroup in look) { byte rating = ratingGroup.Key; Console.WriteLine($"Rating {rating}"); foreach (var recipe in ratingGroup) { Console.WriteLine($" - {recipe.Name}"); } }
Rating 5 - Apple Pie - Banana Pie Rating 2 - Cherry Pie Rating 3 - Beef Pie
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگرهای عناصر Element Operators
این عملگرها، یک توالی ورودی را دریافت و تنها یک عنصر از توالی ورودی و یا یک عنصر را به عنوان عنصر پیش فرض باز میگردانند. این نوع عملگرها توالی خروجی را تولید نمیکنند.
عملگر First
این عملگر اولین عنصر توالی را باز میگرداند.
مثال :
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(); Console.WriteLine(element.Name);
Sugar
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x=>x.Calories==150); Console.WriteLine(element.Name);
Milk
Unhandled Exception: System.InvalidOperationException: Sequence contains no elements
Ingredient[] ingredients = { }; Ingredient element = ingredients.First();
Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.First(x=>x.Calories==1500);
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر FirstOrDefault
عملگر FirstOrDefalt همانند عملگر First عمل میکند، اما با این تفاوت که به جای پرتاب یک استثناء در شرایط معرفی شده در عملگر First، یک مقدار پیش فرض را بر اساس نوع عناصر توالی باز میگرداند. در صورتیکه توالی از نوع عددی باشد، مقدار 0 و اگر عناصر توالی از انواع ارجاعی باشند، مقدار Null و برای مقادیر منطقی، ارزش False بهعنوان مقادیر پیش فرض باز گردانده میشوند.
Ingredient[] ingredients = { }; Ingredient element = ingredients.FirstOrDefault(); Console.WriteLine(element == null);
True
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.FirstOrDefault(x=>x.Calories==1500); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Last
این عملگر آخرین عنصر توالی را باز میگرداند. همچون عملگر First، این عملگر نیز یک امضاء برای دریافت یک عبارت شرط یا پیش بینی دارد. این پیش بینی، آخرین عنصری را که شرط را تامین کند، باز میگرداند. باز هم مثل عملگر First، در صورتی که توالی هیچ عنصری نداشته باشد و یا عدم تامین شرط توسط عناصر توالی، استثنایی رخ خواهد داد.
Ingredient[] ingredients = { new Ingredient {Name = "Sugar", Calories = 500}, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 150}, new Ingredient {Name = "Flour", Calories = 50}, new Ingredient {Name = "Butter", Calories = 500} }; Ingredient element = ingredients.Last(x=>x.Calories==500); Console.WriteLine(element.Name);
Flour
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر LastOrDefault
این عملگر همچون عملگر FirstOrDefault عمل میکند. از بروز استثناء جلوگیری کرده و مقدار پیش فرض را به خروجی ارسال میکند.
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Single
عملگر Single ، تنها عنصر توالی ورودی را باز میگرداند.در صورتی که توالی ما بیش از یک عنصر داشته باشد و یا توالی هیچ عنصری نداشته باشد، یک استثناء رخ خواهد داد.
Unhandled Exception: System.InvalidOperationException: Sequence contains more than one matching element Unhandled Exception: System.InvalidOperationException: Sequence contains no matching element
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 } }; Ingredient element = ingredients.Single(); Console.WriteLine(element.Name);
Sugar
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Butter", Calories = 150}, new Ingredient {Name = "Milk", Calories = 500} }; Ingredient element = ingredients.Single(x => x.Calories == 150); Console.WriteLine(element.Name);
Butter
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر SingleOrDefault
عملگر SingleOrDefault همچون عملگر Single عمل میکند؛ اما با این تفاوت که اگر توالی هیچ عنصری نداشته باشد، مقدار پیش فرض نوع توالی، باز گردانده میشود و در صورتیکه هیچ عنصری شرط مشخص شده را تامین نکند، باز هم مقدار پیش فرض توالی، به جای رخ دادن استثناء باز گردانده میشود.
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.SingleOrDefault(x => x.Calories == 9999); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ElementAt
عملگر ElementAt عنصری را در یک جایگاه مشخص شدهی در توالی، باز میگرداند.
مثال: در کد زیر سومین عنصر توالی ورودی انتخاب میشود:
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAt(2); Console.WriteLine(element.Name);
Milk
System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection.
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر ElementAtOrDefualt
عملگر ElementAtOrDefualt نیز همچون عملگر ElementAt کار میکند؛ اما در صورت وارد کردن اندیسی بزرگتر از اندیس مجاز توالی، دیگر یک استثناء رخ نخواهد داد و یک مقدار پیش فرض، بر اساس نوع عناصر توالی باز گردانده میشود.
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; Ingredient element = ingredients.ElementAtOrDefault(5); Console.WriteLine(element==null);
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر DefaultIfEmpty
عملگر DefaultIfEmpty یک توالی را دریافت کرده و به دو شکل عمل میکند:
1- اگر توالی شامل حداقل یک عنصر باشد، این توالی بدون هیچ تغییری به خروجی ارسال میشود.
2- اگر توالی هیچ عنصری نداشته باشد، توالی خروجی خالی نخواهد بود. در این حالت توالی خروجی تنها یک عضو دارد و آن هم مقدار پیش فرضی بر اساس نوع توالی میباشد.
مثال :
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient {Name = "Egg", Calories = 100}, new Ingredient {Name = "Milk", Calories = 50} }; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item.Name); }
Sugar Egg Milk
کد زیر حالت دوم معرفی شدهی در تعریف DefaultIfEmpty را نشان میدهد.
Ingredient[] ingredients = { }; IEnumerable<Ingredient> query = ingredients.DefaultIfEmpty(); foreach (Ingredient item in query) { Console.WriteLine(item == null); }
True
پیاده سازی توسط عبارتهای جستجو
معادل این عملگر، کلمهی کلیدی جدیدی در عبارتهای جستجو وجود ندارد و ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
مشکل با نوشتن تابع تجمعی سفارشی(از طریق پیاده سازی IAggregateFunction)
public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData) { if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0; var list = columnCellsSummaryData; var lastItem = list.Last(); return lastItem.CellData.PropertyValue; }
Method 'set_DisplayFormatFormula' in type 'Hezareh.Modules.Accounting.Reporting.ViewModels.MySampleAggregateFunction' from assembly 'Hezareh.Modules.Accounting, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.
public class MySampleAggregateFunction : IAggregateFunction { public MySampleAggregateFunction() { } /// <summary> /// Fires before rendering of this cell. /// Now you have time to manipulate the received object and apply your custom formatting function. /// It can be null. /// </summary> public Func<object, string> DisplayFormatFormula { set; get; } #region Fields (6) double _groupAvg; long _groupRowNumber; double _groupSum; double _overallAvg; long _overallRowNumber; double _overallSum; #endregion Fields #region Properties (2) /// <summary> /// Returns current groups' aggregate value. /// </summary> public object GroupValue { get { return _groupAvg; } } /// <summary> /// Returns current row's aggregate value without considering the presence of the groups. /// </summary> public object OverallValue { get { return _overallAvg; } } #endregion Properties #region Methods (4) // Public Methods (1) /// <summary> /// Fires after adding a cell to the main table. /// </summary> /// <param name="cellDataValue">Current cell's data</param> /// <param name="isNewGroupStarted">Indicated starting a new group</param> public void CellAdded(object cellDataValue, bool isNewGroupStarted) { checkNewGroupStarted(isNewGroupStarted); _overallRowNumber++; _groupRowNumber++; double cellValue; if (double.TryParse(cellDataValue.ToSafeString(), NumberStyles.AllowThousands | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out cellValue)) { groupAvg(cellValue); overallAvg(cellValue); } } // Private Methods (3) private void checkNewGroupStarted(bool newGroupStarted) { if (newGroupStarted) { _groupRowNumber = 0; _groupAvg = 0; _groupSum = 0; } } private void groupAvg(double cellValue) { _groupSum += cellValue; _groupAvg = _groupSum / _groupRowNumber; } private void overallAvg(double cellValue) { _overallSum += cellValue; _overallAvg = _overallSum / _overallRowNumber; } /// <summary> /// A general method which takes a list of data and calculates its corresponding aggregate value. /// It will be used to calculate the aggregate value of each pages individually, with considering the previous pages data. /// </summary> /// <param name="columnCellsSummaryData">List of data</param> /// <returns>Aggregate value</returns> public object ProcessingBoundary(IList<SummaryCellData> columnCellsSummaryData) { if (columnCellsSummaryData == null || !columnCellsSummaryData.Any()) return 0; var list = columnCellsSummaryData; var lastItem = list.Last(); return lastItem.CellData.PropertyValue; } #endregion Methods }
columns.AddColumn(column => { column.PropertyName<VoucherRowPrintViewModel>(x => x.CaclulatedRemains); column.CellsHorizontalAlignment(PdfRpt.Core.Contracts.HorizontalAlignment.Right); column.IsVisible(true); column.Order(5); column.Width(1.5f); column.ColumnItemsTemplate(template => { template.TextBlock(); template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.CustomAggregateFunction(new MySampleAggregateFunction()); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); column.HeaderCell("مانده"); });
https://www.dntips.ir/jquery https://www.dntips.ir/mvc https://www.dntips.ir/برنامه
الف) ساختار کنترلر جستجوی سایت
فرض کنید جستجوی سایت در کنترلری به نام Search و توسط اکشن متد پیش فرضی با فرمت زیر مدیریت میشود:
[ValidateInput(false)] //برنامه نویسها نیاز دارند تگها را جستجو کنند public virtual ActionResult Index(string term) {
ب) مدیریت کنترلرهای یافت نشد
اگر از یک IoC Container در برنامهی ASP.NET MVC خود مانند StructureMap استفاده میکنید، نوشتن کد متداول زیر کافی نیست:
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { if (controllerType == null) { var url = requestContext.HttpContext.Request.RawUrl; //string.Format("Page not found: {0}", url).LogException(); requestContext.RouteData.Values["controller"] = MVC.Search.Name; requestContext.RouteData.Values["action"] = MVC.Search.ActionNames.Index; requestContext.RouteData.Values["term"] = url.GetPostSlug().Replace("-", " "); return ObjectFactory.GetInstance(typeof(SearchController)) as Controller; } return ObjectFactory.GetInstance(controllerType) as Controller; } }
ج) مدیریت آدرسهای یافت نشد پسوند دار
تنظیمات فوق کلیه آدرسهای بدون پسوند را مدیریت میکند. اما اگر درخواست رسیده به شکل https://www.dntips.ir/mvc/test/file.aspx بود، خیر. در اینجا حداقل سه مرحله را باید جهت مدیریت و هدایت خودکار آن به صفحهی جستجو انجام داد
- باید فایلهای پسوند دار را وارد سیستم مسیریابی کرد:
routes.RouteExistingFiles = true; //نیاز هست دانلود عمومی فایلها تحت کنترل قرار گیرد
پس از مسیریابی پیش فرض سایت (نه قبل از آن)، مسیریابی ذیل باید اضافه شود:
routes.MapRoute( "CatchAllRoute", // Route name "{*url}", // URL with parameters new { controller = "Search", action = "Index", term = UrlParameter.Optional, area = "" }, // Parameter defaults new { term = new UrlConstraint() } );
مشکل! نیاز است پارامتر term را به صورت پویا مقدار دهی کنیم. برای اینکار میتوان یک RouteConstraint سفارشی نوشت:
public class UrlConstraint : IRouteConstraint { public bool Match(System.Web.HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) { var url = httpContext.Request.RawUrl; //string.Format("Page not found: {0}", url).LogException(); values["term"] = url.GetPostSlug().Replace("-", " "); return true; } }
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
Stored Procedure (که از این به بعد برای ایجاز، SP نوشته خواهد شد) هم از قاعده فوق مستثنی نیست و در صورت انتخاب صحیح میتواند به ارائهی محصول نهایی با کیفیتتری در زمان کوتاهتری کمک کند و در صورت انتخاب ناآگاهانه ممکن است باعث شکست یک پروژه (بخصوص در بلند مدت) شود.
تاریخچه
SQL توسط شرکت IBM در اوایل دهه 70 میلادی ایجاد شد. با اوج گرفتن زبانهای رویهای، SQL هم چندان از این قافله عقب نماند که منجر به پذیرش SP به عنوان یک استاندارد، در دهه 90 میلادی و پیاده سازی تدریجی آن توسط غولهای سازنده دیتابیس شد (رجوع فرمایید به ^ و ^). این فاصله 20 ساله باعث غنیتر شدن SQL شد و وجود SP - به معنی انتقال مدل برنامه نویسی رویهای به SQL - بخشی از مشکلات قبلی کار با کوئریهای پشت سر هم و خام را حل کرد. از سال 2000 میلادی به بعد، ORMهای قدرتمندی از جمله Hibernate و پیاده سازیهای مختلفی از Active Record و Entity Framework متولد شدند. بنابر این تقدم و تاخّرهای زمانی، بدیهی است اغلب مزایای SP نسبت به Raw SQL Query و اغلب معایب آن نسبت به ORMها باشد.
بنظر میرسد برای پاسخ به سوال اصلی این مطلب، ناگزیر به مقایسه SP با رقبای دیرینهاش هستیم. با برشمردن معایب و مزایای SP میتوان به نتیجهی منطقیتری رسید. البته باید در نظر داشت صرف استفاده از SP به معنای بهرهمند شدن از مزایای آن و صرف استفاده نکردن از آن هم بهرهمندی از رقبای آن نیست. چگونگی استفاده یک ابزار، مهمتر از خود ابزار است.
معایب SP
- دستورات Alter Table ، Add Column و Drop Column به این سادگیها هم نیستند؛ ممکن است به یکی از جداول دیتابیس دو ستون اضافه یا از آن حذف شوند. مجبوریم تمامی SPها را بخصوص Insert و Update متناظر با جدول را تغییر دهیم که این تغییرات ممکن است بصورت زنجیرهوار به سایر SPها هم سرایت کند. حال شرایطی را در نظر بگیرید که تعداد SPهای شما به چند ده و یا حتی به چند صد عدد و بیشتر، رسیده باشد که این به معنی زحمت بیشتر و تغییرات پر هزینهتر است.
- احتمال کند شدن ماشین سرویس دهنده در اثر اجرای تعداد
زیادی SP ؛ چناچه بخش زیادی از منطق برنامه از طریق SP اجرا شود، سرور دیتابیس موظف به اجرای آنهاست. اما در صورتیکه منطق،
در کد برنامه قرار داشته باشد، امکان توزیع آن بر روی سرورهای مجزا و یا حتی ماشین
کلاینت وجود خواهد داشت. امروزه اکثر کلاینتها به دیتابیسهای سبک و سریعی مجهز شدهاند. بنابراین در صورت امکان چرا بار پردازشی را به عهده آنها نگذاریم؟!
- یکپارچگی کمتر؛ تقریبا همه اپلیکیشنها نیازمند
ارتباط با سایر سیستمها هستند. اگر بخشهای زیادی از منطق برنامه درون SP مخفی شده باشند، این نقطه تلاقی بین سیستمی، احتمالا
درون خود دیتابیس قرار میگیرد و این به معنی ایجاد SP های بیشتر، افزودن
پارامترهای بیشتر، توسعه SPهای قبلی و بطور
خلاصه اعمال تغییرات بیشتر، که منتج به قابلیت نگهداری کمترخواهد شد.
- انعطاف پذیری کمتر؛ در یک شرایط ایده آل، عملکرد اپلیکیشن، مستقل از دیتابیس است. اگر نیاز به تغییر دیتابیس، مثلا از اوراکل به Microsoft SQL Server وجود داشته باشد، نیاز به بازنویسی و انتقال فانکشنها و SP ها محتمل است و از آنجائیکه که با وجود استانداردها، دیتابیسهای مختلف، معمولا در Syntax دستورات، تفاوتهای فاحشی دارند، هر چه کد بیشتری در SP ها باشد، نیاز به انتقال و تبدیل بیشتری وجود دارد.
- عدم وجود بازخورد مناسب؛ بسیاری از اوقات در صورت بروز اشکالی در حین اجرای یک SP، فقط با یک متن ساده بصورت Table has no rows و یا error مواجه میشویم. چنین خطاهایی هنگام دیباگ اصلا خوشایند نیستند. MS SQL در این بین بازخوردهای مناسبی را ارائه میکند. اگر تجربه کار با سایر دیتابیسها را داشته باشید، اهمیت بازخوردهای مناسب، ملموستر خواهد بود.
- کد نویسی سختتر؛ نوشتن کد SQL معمولا در همان IDE اپلیکیشن انجام نمیشود. جابجایی مداوم بین دو IDE ، دیباگ و کد نویسی از طریق دو اینترفیس مجزا، اصلا ایدهال نیست.
- SP منطق را بیش از حد پنهان میکند؛ حتی با دانستن نام صحیح یک SP، باز هم تصویری از پارامترهای ارسالی به آن و نتیجه برگشتی نخواهیم داشت. نمیدانیم نتیجه حاصل از اجرای SP ما مقداری را برمیگرداند یا خیر؟ در صورت وجود برگشتی، یک Cursor است یا یک مقدار؟ اگر Cursor است شامل چه ستونهایی است؟
- SP نمیتواند یک شیء را به عنوان آرگومان بپذیرد؛ بنابراین احتمال کثیف شدن کد به مرور افزایش پیدا میکند و بدتراز آن، در صورت ارسال اشتباه یک پارامتر، یا عدم تطابق تعداد پارامترها، مجبور به بررسی تمام آنها بصورت دستی هستیم. برای مثال دو قطعه کد زیر را با هم مقایسه کنید:
INSERT INTO User_Table(Id,Username,Password,FirstName,SureName,PhoneNumber,x,Email) VALUES (1,'VahidN','123456','Vahid','Nasiri','09120000000','vahid_xxx@example.com')
و معادل آن در یک ORM فرضی:
public void Insert(User user) { _users.Insert(user); db.Save(); }
بهوضوح قطعه کد sql، قبل از خوب یا بد بودن، زشت است. همچنین پارامتر x آن که فرضاً به تازگی اضافه شده، مقداری را دریافت نکرده و باعث بروز خطا خواهد شد.
- نبود Query Chaining؛ یکی از ویژگیهای جذاب ORMهای امروزی، امکان تشکیل یک کوئری با قابلیت خوانایی بالا و افزودن شرطهای بیشتر از طریق الگوی builder است. قطعه کد زیر یک SP برای جستجوی داینامیک نام و نام خانوادگی در یک جدول فرضی به اسم Users است:
public ICollection<User> GetUsers(string firstName,string lastName,Func<User, bool> orderBy) { var query = _users.where(u => u.LastName.StartsWith(lastName)); query = query.where(u => u.FirstName.StartsWith(firstName)); query = query.OrderBy(orderBy); return query.ToList(); }
در مقایسه با معادل SP آن:
CREATE PROCEDURE DynamicWhere @LastName varchar(50) = null, @FirstName varchar(50) = null, @Orderby varchar(50) = null AS BEGIN DECLARE @where nvarchar(max) SELECT @where = '1 = 1' IF @LastName IS NOT NULL SELECT @Where = @Where + " AND A.LastName LIKE @LastName + '%'" IF @FirstName IS NOT NULL SELECT @Where = @Where + " AND A.FirstName LIKE @FirstName + '%'" DECLARE @orderBySql nvarchar(max) SELECT @orderBySql = CASE WHEN @OrderBy = "LastName" THEN "A.LastName" ELSE @OrderBy = "FirstName" THEN "A.FirstName" END DECLARE @sql nvarchar(max) SELECT @sql = " SELECT A.Id , A.AccountNoId, A.LastName, A.FirstName, A.PostingDt, A.BillingAmount FROM Users WHERE " + @where + " ORDER BY " + @orderBySql exec sp_executesql @sql, N'@LastName varchar(50), @FirstName varchar(50) @LastName, @FirstName END
حاجت به گفتن نیست که قطعه کد اول چقدر خواناتر، انعطاف پذیرتر، خلاصهتر و قابل نگهداریتر است.
- نداشتن امکانات زبانهای مدرن؛ زبانها و IDEهای مدرن، امکانات قابل توجهی را برای نگهداری بهتر، انعطاف پذیری بیشتر، مقیاس پذیری بالاتر، تست پذیری دقیقتر و... ارائه میکنند. به عنوان مثال:
- شیءگرایی و امکانات آن که در SP موجود نیست و در مورد قبلی معایب، به آن مختصرا اشاره شد. در نظر بگیرید اگر SQL زبانی شیء گرا بود و مجهز به ارث بری و کپسوله سازی بود، چقدر قابلیت نگهداری آن بالاتر میرفت و حجم کدهای نوشته شده میتوانست کمتر باشند.
- نداشتن Lazy Loading که باعث مصرف زیاد حافظه میشود.
- نداشتن intellisense حین فراخوانیها.
- نداشتن Navigation Property که باعث join نویسیهای زیاد خواهد شد.
- SQL در مقایسه با یک زبان مدرن ناقص بنظر میرسد و این نوشتن کد آن را سختتر میکند.
- نداشتن امکان تغییر منطقی نام جداول و ستون ها
- مدیریت تراکنشها بصورت دستی، حال آنکه با الگوی Unit Of Work این مشکل در یک ORM قدرتمند مثل EF حل شده است.
- زمان بر بودن نوشتن SP؛ گاهی نوشتن یک تابع در یک ORM یا بعضا نوشتن یک کوئری SQL کوتاه در یک رشته متنی، سادهتر از نوشتن کد SP است. آیا برای هر وظیفه کوچک در دیتابیس، نوشتن یک SP ضروری است؟
مزایای SP :
- کمتر کردن Round Trips در شبکه و متعاقبا کاهش ترافیک شبکه؛ اگر از یک فراخوانی استفاده کنیم، کاهش Round Tripها تاثیر چندانی نخواهد داشت. همچنین ارسال یک کوئری کامل، نسبت به ارسال فقط اسم SP و پارامترهای آن، پهنای باند بیشتری اِشغال میکند. البته در یک شبکه با سرعت قابل قبول، بعید است این دو مزیت محسوس باشند؛ اما به هر حال برای موارد خاص، دو مزیت محسوب میشوند. نکته دیگر آنکه بدلیل Pre-Compiled بودن SPها و همچنین کَش شدن Execution Plan آنها، اندکی با سرعت بالاتری اجرا میشوند.
- امکان چک کردن سینتکس قبل از اجرای آن؛ در مقایسه با Raw Query مزیت محسوب میشود.
- امکان به اشتراک گذاری کد؛ برای پروژههایی که چندین اپلیکیشن با چندین زبان برنامه نویسی مختلف در حال تهیه هستند و نیازمند دسترسی مستقیم به دادهها با سرعت به نسبت بالاتری هستند، SP میتواند یک راه حل ایده آل محسوب شود. بجای پیاده سازی منطق برنامه در هر اپلیکیشن بصورت جداگانه و زحمت کدنویسی هرکدام، میتوان از SP استفاده کرد. هرچند امروزه معمولا برای حل این مشکل، API های مشترک معماری Restful ارجحیت دارد.
- کمک به ایجاد یک پَک؛ در یک زیر سیستم با نیازمندی مشخص که اعمال تغییرات در آن محتمل نمیباشد نیز SP میتواند یک گزینه مناسب به حساب آید. مثلا یک سیستم Membership را در نظر بگیرید که در پروژههای مختلف شما مورد استفاده قرار خواهد گرفت. برای مثال میشود یک سیستم Membership سفارشی را با امکان Hash پسورد و رمز کردن دادههای حساس، به کمک SP و Function های مناسب فراهم کرد و در واقع بین Application Login و Data Logic تمایز قائل شد. شخصا معماری Restful را به این روش هم ترجیح میدهم.
- بهرمند شدن از امکانات بومی SQL ؛ به عنوان نمونه برای ترانهاده کردن خروجی یک کوئری میتوان از فانکشن Pivot استفاده کرد. یا فانکشنهای تحلیلی Lead و Lag (لینک مستندات اوراکل این دو فانکشن به ترتیب در ^ و ^ ) که بنظر نمیرسد هنوز معادل مستقیمی درORM ها داشته باشند.
- تسلط و کنترل بیشتر و دقیقتر بر کوئری نهایی؛ گفته میشود SP و عبارات SQL در دیتابیس، حکم assembly را در سایر زبانها دارند. بنابراین با SP میتوان عبارات SQL و نحوه اجرای آن را در دیتابیس، بطور کامل تحت فرمان داشت. این در حالی است که هر یک از ORMها دستورات زبان برنامه نویسی مبداء را به یک عبارت SQL ترجمه میکنند که این عبارت چندان تحت کنترل برنامه نویس نیست و بیشتر به مدل کاری ORM بستگی دارد.
- امکان join بین دو یا چند دیتابیس مجزا؛ حال آنکه امکان join بین دو Context در ORM ها وجود ندارد. بعلاوه اگر دو دیتابیس مدنظر ما روی دو سرور مجزا باشند، با SP و کانفیگ Linked Server کماکان میشود کوئری join دار نوشت.
- برای عملیاتهای Batch مناسبتر است؛ در مقام مقایسه با ORM ها که با تکنیکهای مختلفی سعی در افزایش سرعت عملیات Batch، بخصوص Insert و Update را دارند، SP با سرعت قابل قبولتری اجرا میشود.
- عدم نیاز به یادگیری سینتکس و ابزاری جدید؛ موارد
بسیاری وجود دارند که فرصت یادگیری تکنولوژی جدیدی مثل یک ORM و یا SQL Bulk و حتی کتابخانههای ثالث مبتنی بر این ابزارها وجود ندارند و ممکن است مجبور شوید برای باقی ماندن در بازار رقابتی، از
دانستههای قبلی خود استفاده کنید .
- تخصصیتر کردن وظایف؛ برنامه نویسهای دیتابیس به صورت تخصصی اقدام به تحلیل روابط و ایندکسها میکنند، دیتابیس را ایجاد و نرمال سازی مینمایند، SP های متناسب را میسازند و به بهترین شکل Optimize و در آخر تست میکنند.
- امنیت به نسبت بالاتر؛ میتوان مجوز اجرای SP را به یک کاربر اعطا کرد، بدون آنکه مجوز دسترسی به جداول مورد استفاده در آن SP را داد. همچنین نسبت به کوئریهای پارامتری نشده، SQL ارجیحت دارند چون احتمال آسیب پذیری در مقابل SQL Injection را کمتر میکنند.
نتیجهگیری
اگرچه SP ها برای پردازش دادهها آنقدر هم که در وبلاگها میخوانیم بد نیستند، اما سوء استفاده از آن، مشکلات عدیدهای را ایجاد خواهد کرد. با توجه به روند تغییرات تکنولوژیهای دسترسی به دادهها و معماریهای مدرن بنظر میرسد SP در بهترین حالت، ابزار مناسبی برای انجام عملیات CRUD است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
نحوهی تعیین فرهنگ ترد جاری در ASP.NET Core
در نگارشهای پیشین ASP.NET، برای تعیین فرهنگ ترد جاری، از یکی از دو روش ذیل استفاده میشود:
الف) افزودن مدخل بومی سازی به فایل web.config
<system.web> <globalization uiCulture="fa-IR" culture="fa-IR" /> </system.web>
protected void Application_BeginRequest() { Thread.CurrentThread.CurrentCulture = new CultureInfo("fa-IR"); Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR"); }
public void Configure(IApplicationBuilder app) { app.UseRequestLocalization(new RequestLocalizationOptions { DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")), SupportedCultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") }, SupportedUICultures = new[] { new CultureInfo("en-US"), new CultureInfo("fa-IR") } });
- تنظیمات SupportedCultures بر روی نمایش تاریخ، ساعت و واحد پولی تاثیر دارند. همچنین میتوانند بر روی نحوهی مقایسهی حروف و مرتب سازی آنها تاثیر داشته باشند.
- تنظیمات SupportedUICultures مشخص میکنند که کدامیک از فایلهای resx برنامه که مداخل ترجمههای آنرا به زبانهای مختلف مشخص میکنند، باید بارگذاری شوند.
- تنظیم DefaultRequestCulture در صورت مشخص نشدن فرهنگ ترد جاری مورد استفاده قرار میگیرد.
یک مثال: هر ترد در دات نت دارای اشیاء CurrentCulture و CurrentUICulture است. اگر فرهنگ ترد جاری به en-US تنظیم شده باشد، متد DateTime.Now.ToLongDateString، خروجی نمونه Thursday, February 18, 2016 را نمایش میدهد.
زمانیکه میان افزار RequestLocalization فعال میشود، سه تامین کنندهی پیش فرض (مقدارهای پیش فرض خاصیت RequestCultureProviders شیء RequestLocalizationOptions فوق)، جهت مشخص ساختن فرهنگ ترد جاری بکار گرفته خواهند شد:
الف) از طریق کوئری استرینگ با فعال سازی QueryStringRequestCultureProvider
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
http://localhost:5000/?culture=es-MX
برای مثال در اینجا QueryStringRequestCultureProvider به دنبال کوئری استرینگهای culture و یا ui-culture گشته و با رسیدن به es-MX، فرهنگ جاری را به اسپانیایی مکزیکی تنظیم میکند. در این حالت اگر فقط culture ذکر شود، ui-culture نیز به همان مقدار تنظیم خواهد شد.
ب) از طریق نام کوکی با فعال سازی CookieRequestCultureProvider
CookieRequestCultureProvider کوکی ویژهای را با نام پیش فرض AspNetCore.Culture. ایجاد میکند. این کوکی برای ردیابی اطلاعات بومی سازی انتخابی کاربر بکار میرود. برای مثال اگر به مقدار ذیل تنظیم شود:
c='en-UK'|uic='en-US'
ج) از طریق هدر مخصوص Accept-Language با فعال سازی AcceptLanguageHeaderRequestCultureProvider که میتواند به همراه درخواست HTTP ارسال شود.
اگر تمام این حالتها تنظیم نشده بودند، آنگاه از مقدارDefaultRequestCulture استفاده میشود. برای مثال اگر مرورگر به صورت پیش فرض هدر Accept-Language را en-US ارسال میکند :
دیگر کار به پردازش مقدارDefaultRequestCulture نخواهد رسید.
اکنون اگر علاقمند بودید تا به کاربر امکان انتخاب زبانی را بدهید، یک چنین اکشن متدی را طراحی کنید:
public IActionResult SetFaLanguage() { Response.Cookies.Append( CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))), new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) } ); return RedirectToAction("GetTitle"); }
از اینجا به بعد است که اگر نام کنترلر شما TestLocalController باشد، فایل منبع متناظر با آن یعنی Controllers.TestLocalController.fa.resx، به صورت خودکار بارگذاری و پردازش خواهد شد. در غیر اینصورت فایل نمونهی ختم شدهی به en.resx پردازش میشود؛ چون این زبان به صورت پیش فرض در هدر Accept-Language قید شدهاست.
آماده سازی برنامه برای کار با فایلهای منبع زبانهای مختلف
ابتدا پوشهی جدیدی را به نام Resources به ریشهی پروژه اضافه کنید. سپس به کلاس آغازین برنامه مراجعه کرده و محل یافت شدن این پوشه را معرفی کنید:
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddMvc() .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization();
به علاوه به سرویس ASP.NET MVC، تنظیمات بومی سازی Viewها و DataAnnotations نیز اضافه شدهاند. تنظیم suffix به معنای view file suffix و یا مثلا fr در نام فایل Index.fr.cshtml است.
نحوهی تعریف و پوشه بندی فایلهای منبع زبانهای مختلف
تا اینجا پوشهی جدید Resources را به پروژه اضافه، معرفی و سرویسهای مرتبط را نیز فعال کردیم. پس از آن نوبت به افزودن فایلهای resx است. برای این منظور بر روی پوشهی منابع کلیک راست کرده و گزینهی add->new item را انتخاب کنید.
در اینجا با جستجوی resource، میتوان فایل resx جدیدی را به پروژه اضافه کرد؛ اما ... انتخاب نام آن باید بر اساس نکات ذیل باشد:
الف) برای کنترلرها یکی از دو مسیر / دار و یا نقطه دار جستجو میشوند:
Resources/Controllers.HomeController.fr.resx
Resources/Controllers/HomeController.fr.resx
در اینجا fr ذکر شده، همان LanguageViewLocationExpanderFormat.Suffix است که پیشتر بحث شد. قسمت ابتدایی Controllers همیشه ثابت است (یا به صورت نام یک پوشه و یا به عنوان قسمت اول نام فایل). سپس نام کلاس کنترلر به همراه نام فرهنگ مدنظر باید ذکر شوند. قسمت نام پوشهی Resources را نیز به services.AddLocalization معرفی کردهایم.
ب) برای Viewها نیز همان حالتهای / دار و یا نقطه دار بررسی میشوند:
Resources/Views.Home.About.fr.resx
Resources/Views/Home/About.fr.resx
برای تمام فایلها و کلاسها میتوان فایل منبع ایجاد کرد
در این نگارش از ASP.NET، در حالت کلی، نام یک فایل منبع، همان نام کامل کلاس آن است؛ منهای فضای نام آن (اگر این فایل منبع در همان اسمبلی قرار گیرد). برای مثال اگر میخواهید برای کلاس Startup برنامه، فایل منبعی را درست کنید و نام کامل آن با درنظر گرفتن فضای نام، معادل LocalizationWebsite.Web.Startup است، ابتدای فضای نام آنرا حذف کنید و سپس آنرا ختم به fa.resx کنید؛ مثلا Startup.fa.resx
اگر محل واقع شدن فایلهای resx در همان اسمبلی اصلی پروژه باشند، نیازی به ذکر فضای نام پیش فرض پروژه نیست. برای مثال اگر فضای نام پیش فرض پروژهی وب جاری MyLocalizationWebsite.Web است، بجای نام فایل MyLocalizationWebsite.Web.Controllers.HomeController.fr.resx میتوانید به صورت خلاصه بنویسید Controllers.HomeController.fr.resx. در غیراینصورت (استفاده از اسمبلیهای دیگر)، ذکر کامل فضای نام مرتبط هم الزامی است.
چند نکته:
- اگر ResourcesPath را در services.AddLocalization معرفی نکنید، مسیر پیش فرض یافتن فایلهای resx مربوط به کنترلرها، پوشهی ریشهی پروژه است و برای Viewها، همان پوشهی محل واقع شدن View متناظر خواهد بود.
- اینکه کدام فایل منبع در برنامه بارگذاری میشود، دقیقا مرتبط است با فرهنگ ترد جاری و این فرهنگ به صورت پیش فرض en-US است (چون همواره در هدر Accept-Language ارسالی توسط مرورگر وجود دارد). برای تغییر آن، از نکتهی اکشن متد public IActionResult SetFaLanguage ابتدای بحث استفاده کنید (در غیراینصورت در آزمایشات خود شاهد بارگذاری فایلهای منبع دیگری بجز en.resxها نخواهید بود).
- فایلهای منبع را به صورت کامپایل شده در پوشهی bin برنامه خواهید یافت:
خواندن اطلاعات منابع در کنترلرهای برنامه
فرض کنید کنترلری را به نام TestLocalController ایجاد کردهایم. بنابراین فایل منبع فارسی متناظر با آن Controllers.TestLocalController.fa.resx خواهد بود؛ با این محتوای نمونه:
محتوای این کنترلر نیز به صورت ذیل است:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; namespace Core1RtmEmptyTest.Controllers { public class TestLocalController : Controller { private readonly IStringLocalizer<TestLocalController> _stringLocalizer; private readonly IHtmlLocalizer<TestLocalController> _htmlLocalizer; public TestLocalController( IStringLocalizer<TestLocalController> stringLocalizer, IHtmlLocalizer<TestLocalController> htmlLocalizer) { _stringLocalizer = stringLocalizer; _htmlLocalizer = htmlLocalizer; } public IActionResult Index() { var name = "DNT"; var message = _htmlLocalizer["<b>Hello</b><i> {0}</i>", name]; ViewData["Message"] = message; return View(); } [HttpGet] public string GetTitle() { var about = _stringLocalizer["About Title"]; return about; } public IActionResult SetFaLanguage() { Response.Cookies.Append( CookieRequestCultureProvider.DefaultCookieName, CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))), new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) } ); return RedirectToAction("GetTitle"); } } }
اگر برنامه را در حالت معمولی اجرا کنید و سپس آدرس http://localhost:7742/testlocal/gettitle را درخواست کنید، عبارت About Title را مشاهده میکنید؛ به دو علت:
الف) هنوز فرهنگ پیش فرض ترد جاری همان en-US است که توسط مرورگر ارسال شدهاست.
ب) چون فایل resx متناظر با فرهنگ پیش فرض ترد جاری یافت نشدهاست، مقدار همان کلید درخواستی بازگشت داده میشود؛ یعنی همان About Title.
برای رفع این مشکل آدرس http://localhost:7742/testlocal/SetFaLanguage را درخواست کنید. به این صورت با تنظیم کوکی ردیابی فرهنگ ترد جاری به زبان فارسی، خروجی GetTile اینبار «درباره» خواهد بود.
خواندن اطلاعات منابع در Viewهای برنامه
فرض کنید فایل Views.TestLocal.Index.fa.resx (فایل منبع کنترلر TestLocal و ویوو Index آن به زبان فارسی) دقیقا همان محتوای فایل Controllers.TestLocalController.fa.resx فوق را دارد (اگر نام پوشهی Views را تغییر دادهاید، قسمت ابتدایی نام فایل Views را هم باید تغییر دهید). برای دسترسی به اطلاعات آن در یک ویوو، میتوان از سرویس IViewLocalizer به نحو ذیل استفاده کرد:
@using Microsoft.AspNetCore.Mvc.Localization @inject IViewLocalizer Localizer @{ } Message @ViewData["Message"] <br/> @Localizer["<b>Hello</b><i> {0}</i>", "DNT"] <br/> @Localizer["About Title"]
Localizer از طریق تزریق سرویس IViewLocalizer به View برنامه تامین میشود. این سرویس در پشت صحنه از همان IHtmlLocalizer استفاده میکند و در حین استفادهی از آن، اطلاعات تگها انکد (encoded) نخواهند شد (به همین جهت برای کار با کلیدها و مقادیر تگدار توصیه میشود).
استفاده از اطلاعات منابع در DataAnnotations
قسمت اول فعال سازی بومی سازی DataAnnotations با ذکر AddDataAnnotationsLocalization در متد ConfigureServices، در ابتدای بحث انجام شد و همانطور که پیشتر نیز عنوان گردید، در این نگارش از ASP.NET، برای تمام کلاسهای برنامه میتوان فایل منبع ایجاد کرد. برای مثال اگر کلاس RegisterViewModel در فضای نام ViewModels.Account قرار گرفتهاست، نام فایل منبع آن یکی از دو حالت / دار و یا نقطه دار ذیل میتواند باشد:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx
محتوای این کلاس را در ذیل مشاهده میکنید:
using System.ComponentModel.DataAnnotations; namespace Core1RtmEmptyTest.ViewModels.Account { public class RegisterViewModel { [Required(ErrorMessage = "EmailReq")] [EmailAddress(ErrorMessage = "EmailType")] [Display(Name = "Email")] public string Email { get; set; } } }
یک نکته: هیچ الزامی ندارد که کلیدها را به این شکل وارد کنید. از این جهت که اگر این کلید در فایل منبع یافت نشد و یا فرهنگ ترد جاری با فایلهای منبع مهیا تطابقی نداشت، عبارتی را که کاربر مشاهده میکند، دقیقا معادل «EmailReq» خواهد بود. بنابراین در اینجا میتوانید کلید را به صورت کامل، مثلا مساوی «The Email field is required» وارد کنید و همین عبارت را به عنوان کلید در فایل منبع ذکر کرده و مقدار آنرا مساوی ترجمهی آن قرار دهید. این نکته در تمام حالات کار با کنترلرها و ویووها نیز صادق است.
استفاده از یک منبع اشتراکی
اگر میخواهید تعدادی از منابع را در همهجا در اختیار داشته باشید، روش کار به این صورت است:
الف) یک کلاس خالی را به نام SharedResource دقیقا با فرمت ذیل در پوشهی Resources ایجاد کنید:
// Dummy class to group shared resources namespace Core1RtmEmptyTest { public class SharedResource { } }
SharedResource.fa.resx
SharedResource.en-US.resx
و امثال آن
ج) برای استفادهی از این منبع اشتراکی در کلاسهای مختلف برنامه تنها کافی است در حین تزریق وابستگیها، نوع آرگومان جنریک IStringLocalizer را به SharedResource تنظیم کنید:
IStringLocalizer<SharedResource> sharedLocalizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer