الف) در متدهای لایه جاری خود واژههای کلیدی new و همچنین کلیه فراخوانیهای استاتیک را بیابید.
ب) وهله سازی اینها را به یک سطح بالاتر (نقطه آغازین برنامه) منتقل کنید. اینکار باید بر اساس اتکای به Abstraction و برای مثال استفاده از اینترفیسها صورت گیرد.
ج) اینکار را آنقدر تکرار کنید تا دیگر در کدهای لایه جاری خود واژه کلیدی new یا فراخوانی متدهای استاتیک مشاهده نشود.
د) در آخر وهله سازی object graphهای مورد نیاز را به یک IoC Container محول کنید.
یک مثال: ابتدا بررسی یک قطعه کد متداول
using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Web.Mvc; namespace DI06.Controllers { public class HomeController : Controller { public ActionResult Index() { string result = string.Empty; using (var client = new WebClient { Encoding = Encoding.UTF8 }) { result = client.DownloadString("https://www.dntips.ir/"); } var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(result); var title = match.Groups[1].Value.Trim(); ViewBag.PageTitle = title; return View(); } } }
مشکلات کد فوق:
الف) قرار گرفتن منطق تجاری پیاده سازی کدها مستقیما داخل کدهای یک اکشن متد؛ این مساله در دراز مدت به تکرار شدید کدها منجر خواهد شد که نهایتا قابلیت نگهداری آنرا کاهش میدهند.
ب) در این کد حداقل دو بار واژه کلیدی new ذکر شده است. مورد اول یا new WebClient، از همه مهمتر است؛ از این جهت که نوشتن آزمون واحد را برای این کنترلر بسیار مشکل میکند. آزمونهای واحد باید سریع و بدون نیاز به منابع خارجی، قابل اجرا باشند. تعویض آن هم مطابق کدهای تدارک دیده شده کار سادهای نیست.
بهبود کیفیت قطعه کد متداول فوق با استفاده از الگوی معکوس سازی وابستگیها
در اصل معکوس سازی وابستگیها عنوان کردیم لایه بالایی سیستم نباید مستقیما به لایههای زیرین در حال استفاده از آن، وابسته باشد. این وابستگی باید معکوس شده و همچنین بر اساس Abstraction یا برای مثال استفاده از اینترفیسها صورت گیرد.
به همین منظور یک پروژه دیگر را از نوع Class library، مثلا به نام DI06.Services به Solution جاری اضافه میکنیم.
namespace DI06.Services { public interface IWebClientServices { string FetchUrl(string url); string GetWebPageTitle(string url); } } using System.Net; using System.Text; using System.Text.RegularExpressions; namespace DI06.Services { public class WebClientServices : IWebClientServices { public string FetchUrl(string url) { using (var client = new WebClient { Encoding = Encoding.UTF8 }) { return client.DownloadString(url); } } public string GetWebPageTitle(string url) { var html = FetchUrl(url); var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(html); return match.Groups[1].Value.Trim(); } } }
هنوز کار معکوس سازی وابستگیها رخ نداده است. صرفا اندکی تمیزکاری و انتقال پیاده سازی منطق تجاری به یک سری کلاسهایی با قابلیت استفاده مجدد صورت گرفته است. به این ترتیب اگر باگی در این کدها وجود داشته باشد و همچنین از آن در چندین نقطه برنامه استفاده شده باشد، اصلاح این کلاس مرکزی، به یکباره تمامی قسمتهای مختلف برنامه را تحت تاثیر مثبت قرار داده و از تکرار کدها و فراموشی احتمالی بهبود قسمتهای مشابه جلوگیری میکند.
کار معکوس سازی وابستگیها در یک لایه بالاتر صورت خواهد گرفت:
using System.Web.Mvc; using DI06.Services; namespace DI06.Controllers { public class HomeController : Controller { readonly IWebClientServices _webClientServices; public HomeController(IWebClientServices webClientServices) { _webClientServices = webClientServices; } public ActionResult Index() { ViewBag.PageTitle = _webClientServices.GetWebPageTitle("https://www.dntips.ir/"); return View(); } } }
در مورد نحوه تنظیمات اولیه یک IoC Container و یا پیشنیازهای ASP.NET MVC جهت آماده شدن برای تزریق خودکار وابستگیها در سازنده کنترلرها، پیشتر مطالبی را در این سری مطالعه کردهاید؛ در اینجا نیز اصول مورد استفاده یکی است و تفاوتی نمیکند.
- اضافه کردن اسمبلیها به صورت دستی به پروژه و تنظیم Build Action آنها به Embedded Resource
- تنظیم فایل csproj پروژه برای Embed کردن خودکار رفرنسهای پروژه در زمان Build
روش اول
روش دوم
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences"> <ItemGroup> <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" /> <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="'%(AssembliesToEmbed.Extension)' == '.dll'"> <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName> </EmbeddedResource> </ItemGroup> <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" /> </Target> <Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build"> <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" /> </Target>
همانطور که در تصویر بالا نیز مشاهده میکنید، اسمبلیهای ارجاعی برنامه TestApp به صورت Resource به آن اضافه شدهاند.
نحوه بارگذاری اسمبلیهای Embed شده
در پروژههای WPF، در OnStartup event کلاس App و در پروژههای WinForm در متد Main کلاس Program، قطعه کد زیر را وارد میکنیم:
private void App_OnStartup( object sender, StartupEventArgs e ) { AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly; var assembly = Assembly.GetExecutingAssembly(); foreach (var name in assembly.GetManifestResourceNames()) { if ( name.ToLower() .EndsWith( ".resources" ) || !name.ToLower() .EndsWith( ".dll" ) ) continue; EmbeddedAssembly.Load( name, name ); } } static Assembly OnResolveAssembly( object sender, ResolveEventArgs args ) { var fields = args.Name.Split( ',' ); var name = fields[0]; var culture = fields[2]; if ( name.EndsWith( ".resources" ) && !culture.EndsWith( "neutral" ) ) return null; return EmbeddedAssembly.Get( args.Name ); }
با استفاده از رویداد AssemblyResolve می توان اسمبلی Embed شده را در زمانیکه نیاز به آن است، بارگذاری کرد. کد مربوط به کلاس EmbeddedAssembly نیز به این صورت میباشد:
using System; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Security.Cryptography; public static class EmbeddedAssembly { static Dictionary< string, Assembly > _dic; public static void Load( string embeddedResource, string fileName ) { if ( _dic == null ) _dic = new Dictionary< string, Assembly >(); byte[] ba; Assembly asm; var curAsm = Assembly.GetExecutingAssembly(); using ( var stm = curAsm.GetManifestResourceStream( embeddedResource ) ) { if ( stm == null ) return; ba = new byte[(int)stm.Length]; stm.Read( ba, 0, (int)stm.Length ); try { asm = Assembly.Load( ba ); _dic.Add( asm.GetName().Name, asm ); return; } catch { } } bool fileOk; string tempFile; using ( var sha1 = new SHA1CryptoServiceProvider() ) { var fileHash = BitConverter.ToString( sha1.ComputeHash( ba ) ) .Replace( "-", string.Empty ); tempFile = Path.GetTempPath() + fileName; if ( File.Exists( tempFile ) ) { var bb = File.ReadAllBytes( tempFile ); var fileHash2 = BitConverter.ToString( sha1.ComputeHash( bb ) ) .Replace( "-", string.Empty ); fileOk = fileHash == fileHash2; } else { fileOk = false; } } if ( !fileOk ) { File.WriteAllBytes( tempFile, ba ); } asm = Assembly.LoadFile( tempFile ); _dic.Add( asm.GetName().Name, asm ); } public static Assembly Get( string assemblyFullName ) { if ( _dic == null || _dic.Count == 0 ) return null; var name = new AssemblyName( assemblyFullName ).Name; return _dic.ContainsKey( name ) ? _dic[name] : null; } }
با استفاده از متد Load کلاس بالا، کل اسمبلیهایی که بارگذاری شدهاند در یک دیکشنری استاتیک نگهداری میشوند. ابتدا اسمبلیها را با استفاده از []byte بارگذاری میکنیم و در صورتیکه بارگذاری اسمبلی با خطایی مواجه شود، بارگذاری را با استفاده از فایل temp انجام میدهیم (که معمولا برای فایلهای unmanaged این مورد اتفاق میافتد).
با استفاده از متد Get که در زمان نیاز به یک اسمبلی توسط AssemblyResolve فراخوانی میشود، اسمبلی مربوطه از دیکشنری پیدا شده و برگشت داده میشود.
نکته ها
- در صورتیکه بخواهید فایلی را از Embed کردن خودکار (روش دوم) استثناء کنید، باید از Condition استفاده کنید:
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences"> <ItemGroup> <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" /> <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(AssembliesToEmbed.Filename)', '^((?!Microsoft).)*$')) And '%(AssembliesToEmbed.Extension)' == '.dll'"> <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName> </EmbeddedResource> </ItemGroup> <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" /> </Target> <Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build"> <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '^((?!Microsoft).)*$')) Or '%(Extension)' == '.xml'" /> </Target>
- در صورتیکه بعد از اجرای برنامه و یا اجرای به صورت دیباگ با خطای Stackoverflow مواجه شدید که به خاطر ارجاعات زیاد Resourceهای برنامه پیش میآید، کد زیر را به فایل AssemblyInfo، در پوشه Properties اضافه کنید:
[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.MainAssembly)]
- در صورتیکه پروژه شما از نوع Office Add-Ins باشد، باید در کد مربوط به AssemblyResolve را در فایل ThisAddIn.Designer.cs (در صورت عدم تغییر نام) به متد Initialize اضافه کنید و دستور بارگذاری را در متد ThisAddIn_Startup اضافه کنید. نکته خیلی مهم: در فایل csproj حتما در قسمت Condition باید اسمبلیهایی را که با نام Microsoft شروع میشوند، از Embed شدن استثناء کنید و در قسمت DeleteAllReferenceCopyLocalPaths مقدار "AfterTargets="VisualStudioForApplicationsBuild را قرار دهید (تا امکان Build پروژه برای شما باشد) و همچنین پسوند vsto را نیز نباید حذف کنید.
1- چگونه Areaهای استاندارد را تبدیل به یک افزونهی مجزا و منتقل شدهی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایهای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگیها تا ثبت مسیریابیها و امثال آن تدارک ببینیم.
3- چگونه فایلهای CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائهی مجزای آنها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدلها و موجودیتهای خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.
در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژههای ASP.NET MVC بدون ارائه فایلهای View آن
ج) آشنایی با تزریق وابستگیها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build
تبدیل یک Area به یک افزونهی مستقل
روشهای زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آنها در اسمبلیهای دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری میشود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژهی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونهی Razor Generator را نصب کنید.
2) سپس یک پروژهی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شدهاست.
3) در ادامه یک پروژهی معمولی دیگر ASP.NET MVC را نیز به پروژهی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شدهاست.
4) به پروژهی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژهی افزونه، تمام پوشههای غیر Area را حذف کنید. پوشههای Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آنرا نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت میکند و در این حالت دیگر نیازی به پوشههای Controllers و Models و Views واقع شده در ریشهی اصلی پروژهی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
PM> Install-Package RazorGenerator.Mvc
همانطور که در تصویر نیز مشخص شدهاست، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژهی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژهی اصلی و افزونه است:
PM> Enable-RazorGenerator
همچنین هربار که View جدیدی اضافه میشود نیز باید اینکار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آنرا به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator اینکار را به صورت خودکار انجام میدهد.
تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشهی bin پروژهی اصلی، دیگر نیازی به ارائهی فایلهای View آن نیست و تمام اطلاعات کنترلرها، مدلها و Viewها به صورت یکجا از فایل dll افزونهی ارائه شده خوانده میشوند.
کپی کردن خودکار افزونه به پوشهی Bin پروژهی اصلی
پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونهها)، نیاز است فایلهای پوشهی bin آنرا به پوشهی bin پروژهی اصلی کپی کنیم (پروژهی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونهی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژهی افزونه مراجعه کرده و قسمت Build events آنرا به نحو ذیل تنظیم کنید:
در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
تنظیم فضاهای نام کلیه مسیریابیهای پروژه
در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونهی کپی شده به پوشهی bin را دریافت و به Application domain جاری اعمال میکند؛ برای اینکار نیازی به کد نویسی اضافهتری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت میکنید. خطاهای حاصل عنوان میکند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژهی اصلی و دیگری در پروژهی افزونه و مشخص نیست که مسیریابیها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژهی افزونه مراجعه کرده و مسیریابی آنرا به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc; namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea { public class NewsAreaAreaRegistration : AreaRegistration { public override string AreaName { get { return "NewsArea"; } } public override void RegisterArea(AreaRegistrationContext context) { context.MapRoute( "NewsArea_default", "NewsArea/{controller}/{action}/{id}", // تکمیل نام کنترلر پیش فرض new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) } ); } } }
using System.Web.Mvc; using System.Web.Routing; namespace MvcPluginMasterApp { public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمتهای برنامه namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) } ); } } }
طراحی قرارداد پایه افزونهها
تا اینجا با نحوهی تشکیل ساختار هر پروژهی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و امثال آن نیز خواهد داشت. به همین منظور، یک پروژهی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System; using System.Reflection; using System.Web.Optimization; using System.Web.Routing; using StructureMap; namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); MenuItem GetMenuItem(RequestContext requestContext); void RegisterBundles(BundleCollection bundles); void RegisterRoutes(RouteCollection routes); void RegisterServices(IContainer container); } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> //public Action<IUnitOfWork> DatabaseSeeder { get; set; } } public class MenuItem { public string Name { set; get; } public string Url { set; get; } } }
PM> install-package EntityFramework PM> install-package Microsoft.AspNet.Web.Optimization PM> install-package structuremap.web
توضیحات قرار داد IPlugin
از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق میشود. برای مثال فعلا کلاس ذیل را به افزونهی پروژه اضافه نمائید:
using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp.PluginsBase; using StructureMap; namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return null; } public MenuItem GetMenuItem(RequestContext requestContext) { return new MenuItem { Name = "Plugin 1", Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" }) }; } public void RegisterBundles(BundleCollection bundles) { //todo: ... } public void RegisterRoutes(RouteCollection routes) { //todo: add custom routes. } public void RegisterServices(IContainer container) { // todo: add custom services. container.Configure(cfg => { //cfg.For<INewsService>().Use<EfNewsService>(); }); } } }
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحهی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوهی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Areaایی به نام NewsArea، مشاهده میکنید.
بارگذاری و تشخیص خودکار افزونهها
پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آنها را به صورت خودکار یافته و سپس پردازش کنیم. اینکار را به کتابخانهی StructureMap واگذار خواهیم کرد. برای این منظور پروژهی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آنرا به نحو ذیل تغییر دهید:
using System; using System.IO; using System.Threading; using System.Web; using MvcPluginMasterApp.PluginsBase; using StructureMap; using StructureMap.Graph; namespace MvcPluginMasterApp.IoCConfig { public static class SmObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static IContainer Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(cfg => { cfg.Scan(scanner => { scanner.AssembliesFromPath( path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"), // یک اسمبلی نباید دوبار بارگذاری شود assemblyFilter: assembly => { return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName); }); scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically. scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName); }); }); } } }
PM> install-package EntityFramework PM> install-package structuremap.web
کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشهی Bin پروژهی اصلی به structuremap معرفی شدهاست. سپس به آن گفتهایم که تنها اسمبلیهایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوعهای IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.
تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار
در ادامه به پروژهی اصلی مراجعه کرده و در پوشهی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq; using System.Web.Optimization; using System.Web.Routing; using MvcPluginMasterApp; using MvcPluginMasterApp.IoCConfig; using MvcPluginMasterApp.PluginsBase; [assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")] namespace MvcPluginMasterApp { public static class PluginsStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); foreach (var plugin in plugins) { plugin.RegisterServices(SmObjectFactory.Container); plugin.RegisterRoutes(RouteTable.Routes); plugin.RegisterBundles(BundleTable.Bundles); } } } }
دراینجا با استفاده از کتابخانهای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شدهاست)، یک متد PostApplicationStartMethod سفارشی را تعریف کردهایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار میدادیم.
در اینجا با استفاده از structuremap، تمام افزونههای موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آنها به هر افزونه به صورت مستقل، تزریق خواهد شد.
اضافه کردن منوهای خودکار افزونهها به پروژهی اصلی
پس از اینکه کار پردازش اولیهی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشهی shared پروژهی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig @using MvcPluginMasterApp.PluginsBase @{ var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); } @foreach (var plugin in plugins) { var menuItem = plugin.GetMenuItem(this.Request.RequestContext); <li> <a href="@menuItem.Url">@menuItem.Name</a> </li> }
سپس به فایل Layout.cshtml_ پروژهی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آنرا در بین سایر آیتمهای منوی اصلی اضافه میکنیم:
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" }) </div> <div class="navbar-collapse collapse"> <ul class="nav navbar-nav"> <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li> @{ Html.RenderPartial("_PluginsMenu"); } </ul> </div> </div> </div>
بنابراین به صورت خلاصه
1) هر افزونه، یک پروژهی کامل ASP.NET MVC است که پوشههای ریشهی اصلی آن حذف شدهاند و اطلاعات آن توسط یک Area جدید تامین میشوند.
2) تنظیم فضای نام مسیریابیهای تمام پروژهها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، میتوان فایلهای bin هر افزونه را توسط رخداد post-build، به پوشهی bin پروژهی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی میکند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک میگیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
تبدیل عدد به حروف
به طور قطع توابع و کلاسهای تبدیل عدد به حروف، در جعبه ابزار توابع کمکی شما هم پیدا میشوند. روز قبل سعی کردم جهت آزمایش، عدد 3000,000,000,000,000 ریال را با کلاسی که دارم تست کنم و نتیجه overflow یا اصطلاحا ترکیدن سیستم بود! البته اگر مطالب این سایت را دنبال کرده باشید پیشتر در همین راستا مطلبی در مورد نحوهی صحیح بکارگیری توابع تجمعی SQL در این سایت منتشر شده است و جزو الزامات هر سیستمی است (تفاوتی هم نمیکند که به چه زبانی تهیه شده باشد). اگر آنرا رعایت نکردهاید، سیستم شما «روزی» دچار overflow خواهد شد.
در کل این کلاس تبدیل عدد به حروف را به صورت ذیل اصلاح کردم و همچنین دو زبانه است؛ چیزی که کمتر در پیاده سازیهای عمومی به آن توجه شده است:
using System.Collections.Generic;
using System.Linq;
namespace NumberToWordsLib
{
/// <summary>
/// Number to word languages
/// </summary>
public enum Language
{
/// <summary>
/// English Language
/// </summary>
English,
/// <summary>
/// Persian Language
/// </summary>
Persian
}
/// <summary>
/// Digit's groups
/// </summary>
public enum DigitGroup
{
/// <summary>
/// Ones group
/// </summary>
Ones,
/// <summary>
/// Teens group
/// </summary>
Teens,
/// <summary>
/// Tens group
/// </summary>
Tens,
/// <summary>
/// Hundreds group
/// </summary>
Hundreds,
/// <summary>
/// Thousands group
/// </summary>
Thousands
}
/// <summary>
/// Equivalent names of a group
/// </summary>
public class NumberWord
{
/// <summary>
/// Digit's group
/// </summary>
public DigitGroup Group { set; get; }
/// <summary>
/// Number to word language
/// </summary>
public Language Language { set; get; }
/// <summary>
/// Equivalent names
/// </summary>
public IList<string> Names { set; get; }
}
/// <summary>
/// Convert a number into words
/// </summary>
public static class HumanReadableInteger
{
#region Fields (4)
private static readonly IDictionary<Language, string> And = new Dictionary<Language, string>
{
{ Language.English, " " },
{ Language.Persian, " و " }
};
private static readonly IList<NumberWord> NumberWords = new List<NumberWord>
{
new NumberWord { Group= DigitGroup.Ones, Language= Language.English, Names=
new List<string> { string.Empty, "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" }},
new NumberWord { Group= DigitGroup.Ones, Language= Language.Persian, Names=
new List<string> { string.Empty, "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.English, Names=
new List<string> { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen" }},
new NumberWord { Group= DigitGroup.Teens, Language= Language.Persian, Names=
new List<string> { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.English, Names=
new List<string> { "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety" }},
new NumberWord { Group= DigitGroup.Tens, Language= Language.Persian, Names=
new List<string> { "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.English, Names=
new List<string> {string.Empty, "One Hundred", "Two Hundred", "Three Hundred", "Four Hundred",
"Five Hundred", "Six Hundred", "Seven Hundred", "Eight Hundred", "Nine Hundred" }},
new NumberWord { Group= DigitGroup.Hundreds, Language= Language.Persian, Names=
new List<string> {string.Empty, "یکصد", "دویست", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد" , "نهصد" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.English, Names=
new List<string> { string.Empty, " Thousand", " Million", " Billion"," Trillion", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
new NumberWord { Group= DigitGroup.Thousands, Language= Language.Persian, Names=
new List<string> { string.Empty, " هزار", " میلیون", " میلیارد"," تریلیون", " Quadrillion", " Quintillion", " Sextillian",
" Septillion", " Octillion", " Nonillion", " Decillion", " Undecillion", " Duodecillion", " Tredecillion",
" Quattuordecillion", " Quindecillion", " Sexdecillion", " Septendecillion", " Octodecillion", " Novemdecillion",
" Vigintillion", " Unvigintillion", " Duovigintillion", " 10^72", " 10^75", " 10^78", " 10^81", " 10^84", " 10^87",
" Vigintinonillion", " 10^93", " 10^96", " Duotrigintillion", " Trestrigintillion" }},
};
private static readonly IDictionary<Language, string> Negative = new Dictionary<Language, string>
{
{ Language.English, "Negative " },
{ Language.Persian, "منهای " }
};
private static readonly IDictionary<Language, string> Zero = new Dictionary<Language, string>
{
{ Language.English, "Zero" },
{ Language.Persian, "صفر" }
};
#endregion Fields
#region Methods (7)
// Public Methods (5)
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this int number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this uint number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this byte number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this decimal number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this double number, Language language)
{
return NumberToText((long)number, language);
}
/// <summary>
/// display a numeric value using the equivalent text
/// </summary>
/// <param name="number">input number</param>
/// <param name="language">local language</param>
/// <returns>the equivalent text</returns>
public static string NumberToText(this long number, Language language)
{
if (number == 0)
{
return Zero[language];
}
if (number < 0)
{
return Negative[language] + NumberToText(-number, language);
}
return wordify(number, language, string.Empty, 0);
}
// Private Methods (2)
private static string getName(int idx, Language language, DigitGroup group)
{
return NumberWords.Where(x => x.Group == group && x.Language == language).First().Names[idx];
}
private static string wordify(long number, Language language, string leftDigitsText, int thousands)
{
if (number == 0)
{
return leftDigitsText;
}
var wordValue = leftDigitsText;
if (wordValue.Length > 0)
{
wordValue += And[language];
}
if (number < 10)
{
wordValue += getName((int)number, language, DigitGroup.Ones);
}
else if (number < 20)
{
wordValue += getName((int)(number - 10), language, DigitGroup.Teens);
}
else if (number < 100)
{
wordValue += wordify(number % 10, language, getName((int)(number / 10 - 2), language, DigitGroup.Tens), 0);
}
else if (number < 1000)
{
wordValue += wordify(number % 100, language, getName((int)(number / 100), language, DigitGroup.Hundreds), 0);
}
else
{
wordValue += wordify(number % 1000, language, wordify(number / 1000, language, string.Empty, thousands + 1), 0);
}
if (number % 1000 == 0) return wordValue;
return wordValue + getName(thousands, language, DigitGroup.Thousands);
}
#endregion Methods
}
}
دریافت پروژه کامل به همراه Unit tests مرتبط
داستانی از Unicode
<html> <body> <style type="text/css">p {float: left; padding: 0 15px; margin: 0;}</style> <script type="text/javascript"> for (var i=0; i<128; i++) document.writeln ((i%32?'':'<p>') + i + ': ' + String.fromCharCode (i) + '<br>'); </script> </body> </html>
private String ISO = "ISO-8859-"; protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { for (int i = 1; i < 16; i++) { ListItem item = new ListItem(); item.Text = ISO + i.ToString(); item.Value = i.ToString(); DropDownList1.Items.Add(item); } ShowCodes(1); } } protected void DropDownList1_SelectedIndexChanged(object sender, EventArgs e) { if (DropDownList1.SelectedItem != null) { int value = int.Parse(DropDownList1.SelectedValue); ShowCodes(value); } } private void ShowCodes(int value) { Response.Charset = ISO + value; string s = ""; for (int i = 0; i < 256; i++) { char ch = (char)i; s += i + "-" + ch; s += "<br/>";//br tag } Label1.Text = s; }
کد زیر در جاوااسکریپت کاراکترهای یونیکد را در مرز معینی که برایش مشخص کردهایم نشان میدهد:
<html> <body> <style type="text/css">p {float: left; padding: 0 15px; margin: 0;}</style> <script type="text/javascript"> for (var i=0; i<2096; i++) document.writeln ((i%256?'':'<p>') + i + ': ' + String.fromCharCode (i) + '<br>'); </script> </body> </html>
CSS & Unicode
/* cyrillic */ @font-face { font-style: normal; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; } /* greek-ext */ @font-face { font-style: normal; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+1F00-1FFF; } /* greek */ @font-face { font-style: normal; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0370-03FF; } /* vietnamese */ @font-face { font-style: normal; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0102-0103, U+1EA0-1EF1, U+20AB; } /* latin-ext */ @font-face { font-style: normal; src: local('Roboto Regular'), local('Roboto-Regular'), url(http://fonts.gstatic.com/s/roboto/v14/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2'); unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF; }
UTF-8 نجات بخش میشود
- بسیاری از نرم افزارها و پروتکلها هنوز 8 بیتی کار میکنند.
- اگر یک متن انگلیسی ارسال کنید، 8 بیت هم کافی است ولی در این حالت 32 بیت جابجا میشود؛ یعنی 4 برابر و در ارسال و دریافت و پهنای باند برایمان مشکل ایجاد میکند.
مقایسهای بین نسخههای مختلف :
همانطور که میبینید UTF-8 برای کاراکترهای اسکی، از یک بایت و برای دیگر حروف از دوبایت و برای بقیه BMPها از سه بایت استفاده میکند و در صورتی که کاراکتری در ناحیه مکمل supplementary باشد، از چهار بایت استفاده خواهد کرد. UTF-16 از دو بایت برای نمایش کاراکترهای BMP و از 4 بایت برای نمایش کاراکترهای مکمل استفاده میکند و در UTF-32 از 4 بایت برای همه کاراکترها یا کد پوینتها استفاده میشود.
روش اول: استفادهی دستی از اعتبارسنج کتابخانهی Fluent Validation
روشهای زیادی برای استفادهی از قواعد تعریف شدهی توسط کتابخانهی Fluent Validation وجود دارند. اولین روش، فراخوانی دستی اعتبارسنج، در مکانهای مورد نیاز است. برای اینکار در ابتدا نیاز است با اجرای دستور «dotnet add package FluentValidation.AspNetCore»، این کتابخانه را در پروژهی وب خود نیز نصب کنیم تا بتوانیم از کلاسها و متدهای آن استفاده نمائیم. پس از آن، روش دستی کار با کلاس RegisterModelValidator که در قسمت قبل آنرا تعریف کردیم، به صورت زیر است:
using FluentValidationSample.Models; using Microsoft.AspNetCore.Mvc; namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult RegisterValidateManually(RegisterModel model) { var validator = new RegisterModelValidator(); var validationResult = validator.Validate(model); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors[0].ErrorMessage); } // TODO: Save the model return Ok(); } } }
یک نکته: متد الحاقی AddToModelState که در فضای نام FluentValidation.AspNetCore قرار دارد، امکان تبدیل نتیجهی اعتبارسنجی حاصل را به ModelState استاندارد ASP.NET Core نیز میسر میکند:
public IActionResult RegisterValidateManually(RegisterModel model) { var validator = new RegisterModelValidator(); var validationResult = validator.Validate(model); if (!validationResult.IsValid) { validationResult.AddToModelState(ModelState, null); return BadRequest(ModelState); } // TODO: Save the model return Ok(); }
روش دوم: تزریق اعتبارسنج تعریف شده در سازندهی کنترلر
بجای وهله سازی دستی RegisterModelValidator و ایجاد وابستگی مستقیمی به آن، میتوان از روش تزریق وابستگیهای آن نیز استفاده کرد. در این حالت اعتبارسنج RegisterModelValidator با طول عمر Transient به سیستم تزریق وابستگیها معرفی شده:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>(); services.AddControllersWithViews(); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IValidator<RegisterModel> _registerModelValidator; public HomeController(IValidator<RegisterModel> registerModelValidator) { _registerModelValidator = registerModelValidator; } [HttpPost] public IActionResult RegisterValidatorInjection(RegisterModel model) { var validationResult = _registerModelValidator.Validate(model); if (!validationResult.IsValid) { return BadRequest(validationResult.Errors[0].ErrorMessage); } // TODO: Save the model return Ok(); } } }
روش سوم: خودکار سازی اجرای یک تک اعتبارسنج تعریف شده
اگر متد الحاقی AddFluentValidation را به صورت زیر به سیستم معرفی کنیم:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IValidator<RegisterModel>, RegisterModelValidator>(); services.AddControllersWithViews().AddFluentValidation(); }
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { [HttpPost] public IActionResult RegisterValidatorAutomatically(RegisterModel model) { if (!ModelState.IsValid) { // re-render the view when validation failed. return View(model); } // TODO: Save the model return Ok(); } } }
نکته 1: تنظیمات فوق برایASP.NET Web Pages و PageModels نیز یکی است. فقط با این تفاوت که اعتبارسنجها را فقط میتوان به مدلهایی که به صورت خواص یک page model تعریف شدهاند، اعمال کرد و نه به کل page model.
نکته 2: اگر کنترلر شما به ویژگی [ApiController] مزین شده باشد:
namespace FluentValidationSample.Web.Controllers { [Route("[controller]")] [ApiController] public class HomeController : Controller { [HttpPost] public IActionResult RegisterValidatorAutomatically(RegisterModel model) { // TODO: Save the model return Ok(); } } }
{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "|84df05e2-41e0d4841bb61293.", "errors": { "FirstName": [ "'First Name' must not be empty." ] } }
public void ConfigureServices(IServiceCollection services) { // ... // override modelstate services.Configure<ApiBehaviorOptions>(options => { options.InvalidModelStateResponseFactory = context => { var errors = context.ModelState.Values.SelectMany(x => x.Errors.Select(p => p.ErrorMessage)).ToList(); return new BadRequestObjectResult(new { Code = "00009", Message = "Validation errors", Errors = errors }); }; }); }
روش چهارم: خودکار سازی ثبت و اجرای تمام اعتبارسنجهای تعریف شده
و در آخر بجای معرفی دستی تک تک اعتبارسنجهای تعریف شده به سیستم تزریق وابستگیها، میتوان تمام آنها را با فراخوانی متد RegisterValidatorsFromAssemblyContaining، به صورت خودکار از یک اسمبلی خاص استخراج نمود و با طول عمر Transient، به سیستم معرفی کرد. در این حالت متد ConfigureServices به صورت زیر خلاصه میشود:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>() ); }
سازگاری اجرای خودکار FluentValidation با اعتبارسنجهای استاندارد ASP.NET Core
به صورت پیشفرض، زمانیکه FluentValidation اجرا میشود، اگر اعتبارسنج دیگری نیز در سیستم تعریف شده باشد، اجرا خواهد شد. به این معنا که برای مثال میتوان FluentValidation و DataAnnotations attributes و IValidatableObjectها را با هم ترکیب کرد.
اگر میخواهید این قابلیت را غیرفعال کنید و فقط سبب اجرای خودکار FluentValidationها شوید، نیاز است تنظیم زیر را انجام دهید:
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews().AddFluentValidation( fv => { fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>(); fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false; } ); }
واژههای کلیدی جدید and، or و not در C# 9.0
using System; namespace CS9Features { public static class ModernizingACodebase { public static void PropertyPatternToReplaceIsNullorEmpty1() { string hello = null; // Old approach if (!string.IsNullOrEmpty(hello)) { Console.WriteLine($"{hello} has {hello.Length} letters."); } // New approach, with a property pattern if (hello is { Length: > 0 }) { Console.WriteLine($"{hello} has {hello.Length} letters."); } } public static void PropertyPatternToReplaceIsNullorEmpty2() { // For arrays var greetings = new string[2]; greetings[0] = "Hello world"; greetings = null; // Old approach if (greetings != null && !string.IsNullOrEmpty(greetings[0])) { Console.WriteLine($"{greetings[0]} has {greetings[0].Length} letters."); } // New approach if (greetings?[0] is { Length: > 0 } hi) { Console.WriteLine($"{hi} has {hi.Length} letters."); } } } }
string data = null; var result = data ?? "value";
if (data == null) { data = "value"; } var result = data;
برای مثال بسیاری از نتایج بازگشتی از متدها، چند سطحی هستند:
class Response { public string Result { set; get; } public int Code { set; get; } } class WebRequest { public Response GetDataFromWeb(string url) { // ... return new Response { Result = null }; } }
var webData = new WebRequest().GetDataFromWeb("https://www.dntips.ir/"); if (webData != null && webData.Result != null) { Console.WriteLine(webData.Result); }
در این حالت اگر اشارهگر را به محل && انتقال دهیم، افزونهی ReSharper پیشنهاد یکی کردن این بررسیها را ارائه میدهد:
به این ترتیب تمام چند سطح بررسی نال، به یک عبارت بررسی .? دار، خلاصه خواهد شد:
if (webData?.Result != null) { Console.WriteLine(webData.Result); }
البته باید دقت داشت که برای تمام سطوح باید از .? استفاده کرد (برای مثال response?.Results?.Status)؛ در غیر اینصورت همانند سابق در صورت استفادهی از دات معمولی، به یک null reference exception میرسیم.
کار با متدها و Delegates
این عملگر جدید مقایسهی با نال را بر روی متدها (علاوه بر خواص و فیلدها) نیز میتوان بکار برد. برای مثال خلاصه شدهی فراخوانی ذیل:
if (x != null) { x.Dispose(); }
x?.Dispose();
و یا بکار گیری آن بر روی delegates (روش قدیمی):
var copy = OnMyEvent; if (copy != null) { copy(this, new EventArgs()); }
OnMyEvent?.Invoke(this, new EventArgs());
استفاده از Null Conditional Operator بر روی Value types
الف) مقایسه با نال
کد ذیل را درنظر بگیرید:
var code = webData?.Code;
if (webData?.Code > 0) { }
ب) بازگشت مقدار پیش فرض دیگری بجای نال
اگر نیاز بود بجای null مقدار پیش فرض دیگری را بازگشت دهیم، میتوان از null-coalescing operator سابق استفاده کرد:
int count = response?.Results?.Count ?? 0;
ج) دسترسی به مقدار Value یک متغیر nullable
نمونهی دیگر آن قطعه کد ذیل است:
int? x = 10; //var value = x?.Value; // invalid Console.WriteLine(x?.ToString());
کار با indexer property و بررسی نال
اگر به عنوان بحث دقت کرده باشید، یک s جمع در انتهای Null-conditional operators ذکر شدهاست. به این معنا که این عملگر مقایسهی با نال، صرفا یک شکل و فرم .? را ندارد. مثال ذیل در حین کار با آرایهها و لیستها بسیار مشاهده میشود:
if (response != null && response.Results != null && response.Results.Addresses != null && response.Results.Addresses[0] != null && response.Results.Addresses[0].Zip == "63368") { }
if(response?.Results?.Addresses?[0]?.Zip == "63368") { }
موارد استفادهی ناصحیح از عملگرهای مقایسهی با نال
خوب، عملگر .? کار مقایسهی با نال را خصوصا در دسترسیهای چند سطحی به خواص و متدها بسیار ساده میکند. اما آیا باید در همه جا از آن استفاده کرد؟ آیا باید از این پس کلا استفاده از دات را فراموش کرد و بجای آن از .? در همه جا استفاده کرد؟
مثال ذیل را درنظر بگیرید:
public void DoSomething(Customer customer) { string address = customer?.Employees ?.SingleOrDefault(x => x.IsAdmin)?.Address?.ToString(); SendPackage(address); }
روش بهتر انجام اینکار، بررسی وضعیت customer و انتقال مابقی زنجیرهی LINQ به یک متد مجزای دیگر است:
public void DoSomething(Customer customer) { Contract.Requires(customer != null); string address = customer.GetAdminAddress(); SendPackage(address); }
ASP.NET MVC #22
تهیه سایتهای چند زبانه و بومی سازی نمایش اطلاعات در ASP.NET MVC
زمانیکه دات نت فریم ورک نیاز به انجام اعمال حساس به مسایل بومی را داشته باشد، ابتدا به مقادیر تنظیم شده دو خاصیت زیر دقت میکند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر این اساس دات نت میتواند تشخیص دهد که برای مثال خروجی متد DateTime.Now.ToString در کانادا و آمریکا باید با هم تفاوت داشته باشند. مثلا در آمریکا ابتدا ماه، سپس روز و در آخر سال نمایش داده میشود و در کانادا ابتدا سال، بعد ماه و در آخر روز نمایش داده خواهد شد. یا نمونهی دیگری از این دست میتواند نحوه نمایش علامت واحد پولی کشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روی بارگذاری فایلهای مخصوصی به نام Resource، تاثیر گذار است.
این خواص را یا به صورت دستی میتوان تنظیم کرد و یا ASP.NET، این اطلاعات را از هدر Accept-Language دریافتی از مرورگر کاربر به صورت خودکار مقدار دهی میکند. البته برای این منظور نیاز است یک سطر زیر را به فایل وب کانفیگ برنامه اضافه کرد:
<system.web>
<globalization culture="auto" uiCulture="auto" />
یا اگر نیاز باشد تا برنامه را ملزم به نمایش اطلاعات Resource مرتبط با فرهنگ بومی خاصی کرد نیز میتوان در همین قسمت مقادیر culture و uiCulture را دستی تنظیم نمود و یا اگر همانند برنامههایی که چند لینک را بالای صفحه نمایش میدهند که برای مثال به نگارشهای فارسی/عربی/انگلیسی اشاره میکند، اینکار را با کد نویسی نیز میتوان انجام داد:
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");
جهت آزمایش این مطلب، ابتدا تنظیم globalization فوق را به فایل وب کانفیگ برنامه اضافه کنید. سپس به مسیر زیر در IE مراجعه کنید:
IE -> Tools -> Intenet options -> Genarl tab -> Languages
در اینجا میتوان هدر Accept-Language را مقدار دهی کرد. برای نمونه اگر مقدار زبان پیش فرض را به فرانسه تنظیم کنیم (به عنوان اولین زبان تعریف شده در لیست) و سپس سعی در نمایش مقدار decimal زیر را داشته باشیم:
string.Format("{0:C}", 10.5M)
اگر زبان پیش فرض، انگلیسی آمریکایی باشد، $ نمایش داده خواهد شد و اگر زبان به فرانسه تنظیم شود، یورو در کنار عدد مبلغ نمایش داده میشود.
تا اینجا تنها با تنظیم culture=auto به این نتیجه رسیدهایم. اما سایر قسمتهای صفحه چطور؟ برای مثال برچسبهای نمایش داده شده را چگونه میتوان به صورت خودکار بر اساس Accept-Language مرجح کاربر تنظیم کرد؟ خوشبختانه در دات نت، زیر ساخت مدیریت برنامههای چند زبانه به صورت توکار وجود دارد که در ادامه به بررسی آن خواهیم پرداخت.
آشنایی با ساختار فایلهای Resource
فایلهای Resource یا منبع، در حقیقت فایلهایی هستند مبتنی بر XML با پسوند resx و هدف آنها ذخیره سازی رشتههای متناظر با فرهنگهای مختلف میباشد و برای استفاده از آنها حداقل یک فایل منبع پیش فرض باید تعریف شود. برای نمونه فایل mydata.resx را در نظر بگیرید. برای ایجاد فایل منبع اسپانیایی متناظر، باید فایلی را به نام mydata.es.resx تولید کرد. البته نوع فرهنگ مورد استفاده را کاملتر نیز میتوان ذکر کرد برای مثال mydata.es-mex.resx جهت فرهنگ اسپانیایی مکزیکی بکارگرفته خواهد شد، یا mydata.fr-ca.resx به فرانسوی کانادایی اشاره میکند. سپس مدیریت منابع دات نت فریم ورک بر اساس مقدار CurrentUICulture جاری، اطلاعات فایل متناظری را بارگذاری خواهد کرد. اگر فایل متناظری وجود نداشت، از اطلاعات همان فایل پیش فرض استفاده میگردد.
حین تهیه برنامهها نیازی نیست تا مستقیما با فایلهای XML منابع کار کرد. زمانیکه اولین فایل منبع تولید میشود، به همراه آن یک فایل cs یا vb نیز ایجاد خواهد شد که امکان دسترسی به کلیدهای تعریف شده در فایلهای XML را به صورت strongly typed میسر میکند. این فایلهای خودکار، تنها برای فایل پیش فرض mydata.resx تولید میشوند،از این جهت که تعاریف اطلاعات سایر فرهنگهای متناظر نیز باید با همان کلیدهای فایل پیش فرض آغاز شوند. تنها «مقادیر» کلیدهای تعریف شده در کلاسهای منبع متفاوت هستند.
اگر به خواص فایلهای resx در VS.NET دقت کنیم، نوع Build action آنها به embedded resource تنظیم شده است.
مثالی جهت بررسی استفاده از فایلهای Resource
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. فایل وب کانفیگ آنرا ویرایش کرده و تنظیمات globalization ابتدای بحث را به آن اضافه کنید. سپس مدل، کنترلر و View متناظر با متد Index آنرا با محتوای زیر به پروژه اضافه نمائید:
namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}
using System.Web.Mvc;
using MvcApplication19.Models;
namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}
@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>
قصد داریم در View فوق بر اساس uiCulture کاربر مراجعه کننده به سایت، برچسب Name را مقدار دهی کنیم. اگر کاربری از ایران مراجعه کند، «نام کارمند» نمایش داده شود و سایر کاربران، «Employee Name» را مشاهده کنند. همچنین این تغییرات باید بر روی متد Html.DisplayForModel نیز تاثیرگذار باشد.
برای این منظور بر روی پوشه Views/Home که محل قرارگیری فایل Index.cshtml فوق است کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس در صفحه ظاهر شده، گزینه «Resources file» را انتخاب کرده و برای مثال نام Index_cshtml.resx را وارد کنید.
به این ترتیب اولین فایل منبع مرتبط با View جاری که فایل پیش فرض نیز میباشد ایجاد خواهد شد. این فایل، به همراه فایل Index_cshtml.Designer.cs تولید میشود. سپس همین مراحل را طی کنید، اما اینبار نام Index_cshtml.fa.resx را حین افزودن فایل منبع وارد نمائید که برای تعریف اطلاعات بومی ایران مورد استفاده قرار خواهد گرفت. فایل دومی که اضافه شده است، فاقد فایل cs همراه میباشد.
اکنون فایل Index_cshtml.resx را در VS.NET باز کنید. از بالای صفحه، به کمک گزینه Access modifier، سطح دسترسی متدهای فایل cs همراه آنرا به public تغییر دهید. پیش فرض آن internal است که برای کار ما مفید نیست. از این جهت که امکان دسترسی به متدهای استاتیک تعریف شده در فایل خودکار Index_cshtml.Designer.cs را در View های برنامه، نخواهیم داشت. سپس دو جفت «نام-مقدار» را در فایل resx وارد کنید. مثلا نام را Name و مقدار آنرا «Employee Name» و سپس نام دیگر را NameIsNotRight و مقدار آنرا «Name is required» وارد نمائید.
در ادامه فایل Index_cshtml.fa.resx را باز کنید. در اینجا نیز دو جفت «نام-مقدار» متناظر با فایل پیش فرض منبع را باید وارد کرد. کلیدها یا نامها یکی است اما قسمت مقدار اینبار باید فارسی وارد شود. مثلا نام را Name و مقدار آنرا «نام کارمند» وارد نمائید. سپس کلید یا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائید» را تنظیم نمائید.
تا اینجا کار تهیه فایلهای منبع متناظر با View جاری به پایان میرسد.
در ادامه با کمک فایل Index_cshtml.Designer.cs که هربار پس از تغییر فایل resx متناظر آن به صورت خودکار توسط VS.NET تولید و به روز میشود، میتوان به کلیدها یا نامهایی که تعریف کردهایم، در قسمتهای مختلف برنامه دست یافت. برای نمونه تعریف کلید Name در این فایل به نحو زیر است:
namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}
بنابراین برای استفاده از آن در هر View ایی تنها کافی است بنویسیم:
@MvcApplication19.Views.Home.Index_cshtml.Name
به این ترتیب بر اساس تنظیمات محلی کاربر، اطلاعات به صورت خودکار از فایلهای Index_cshtml.fa.resx فارسی یا فایل پیش فرض Index_cshtml.resx، دریافت میگردد.
علاوه بر امکان دسترسی مستقیم به کلیدهای تعریف شده در فایلهای منبع، امکان استفاده از آنها توسط data annotations نیز میسر است. در این حالت میتوان مثلا پیغامهای اعتبار سنجی را بومی کرد یا حین استفاده از متد Html.DisplayForModel، بر روی برچسب نمایش داده شده خودکار، تاثیر گذار بود. برای اینکار باید اندکی مدل برنامه را ویرایش کرد:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}
همانطور که ملاحظه میکنید، حین تعریف ویژگیهای Display یا Required، امکان تعریف نام کلاس متناظر با فایل resx خاصی وجود دارد. به علاوه ErrorMessageResourceName به نام یک کلید در این فایل و یا پارامتر Name ویژگی Display نیز به نام کلیدی در فایل منبع مشخص شده، اشاره میکنند. این اطلاعات توسط متدهای Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودکار مورد استفاده قرار خواهند گرفت.
نکتهای در مورد کش کردن اطلاعات
در این مثال اگر فیلتر OutputCache را بر روی متد Index تعریف کنیم، حتما نیاز است به هدر Accept-Language نیز دقت داشت. در غیراینصورت تمام کاربران، صرفنظر از تنظیمات بومی آنها، یک صفحه را مشاهده خواهند کرد:
[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()