[HttpGet] public IHttpActionResult SelectAllFake() { var result = new List<KpiTypeView> { new KpiTypeView() {KpiTypeID = 1, KpiTypeName = "افزایشی"}, new KpiTypeView() {KpiTypeID = 2, KpiTypeName = "کاهشی"} }; return Json(result); }
همان طور که در کد زیر مشاهد مینمایید، این کد تشکیل شده است از نوشته لینک و نام اکشن متد و نام کنترلر آن، همراه با آی دی و تعریف css. کاری که قرار است صورت گیرد همانند ActionLink میباشد، با این تفاوت که تصویری هم به لینک ما اضافه خواهد شد:
@Html.ActionLink("Text to display", "ActionNameHere", "ControllerNameHere", new { id = 123 }, new{@class="myClass"})
public MvcHtmlString ActionImage(this HtmlHelper htmlHelper, string controller, string action, object routeValues, string imagePath, string alternateText = "", object htmlAttributes = null) { var anchorBuilder = new TagBuilder("a"); var url = new UrlHelper(HtmlHelper.ViewContext.RequestContext); anchorBuilder.MergeAttribute("href",url.Action(action,controller,routeValues)); var imgBuilder = new TagBuilder("img"); imgBuilder.MergeAttribute("src", url.Content(imagePath)); imgBuilder.MergeAttribute("alt", alternateText); var attributes = (IDictionary<string, object>) HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes); imgBuilder.MergeAttributes(attributes); string imgHtml = imgBuilder.ToString(TagRenderMode.SelfClosing); anchorBuilder.InnerHtml = imgHtml; return MvcHtmlString.Create(anchorBuilder.ToString()); }
4 پارامتر بعدی، پارامترهای ضروری هستند. یکی controller، پس از آن Action routeValues و سپس آدرس تصویر است. در نهایت، ما میتوانیم در صورت نیاز متن جایگزین و ویژگیهای اضافی متنی را نیز مشخص نماییم.
در رابطه با توضیحات مربوط به بدنه متد: ابتدا لینکها را ایجاد خواهد کرد با استفاده از RequestContext و UrlHelper. سپس بخش ساخت تصویر و اضافه کردن آن به لینک است. برای استفاده از میتوان از کد زیر استفاده نمود:
@Html.ActionImage("Workout", "Delete", new { id = @workout.Id }, "/images/bluetrash.png", "Delete", new { id="deletebutton"})
در شکل بالا خروجی HTML تولید شده را مشاهد مینمایید.
context.Chapters.Add(new Chapter { Title = "آزمایش متن فارسی", Text = "برای نمونه تهیه شدهاست", User = user1.Entity });
کامپایل افزونهی spell fix1
افزونهی spell fix، به همراه هیچکدام از توزیعهای باینری SQLite ارائه نمیشود. ارائهی آن فقط به صورت سورس کد است و باید خودتان آنرا کامپایل کنید!
برای این منظور ابتدا به آدرس https://www.sqlite.org/src/dir?ci=99749d4fd4930ccf&name=ext/misc مراجعه کرده و فایل ext/misc/spellfix.c آنرا دریافت کنید. اگر بر روی لینک spellfix.c کلیک کنید، در نوار ابزار بالای صفحهی بعدی، لینک download آن هم وجود دارد.
سپس به صفحهی دریافت اصلی SQLite یعنی https://www.sqlite.org/download.html مراجعه کرده و بستهی amalgamation آنرا دریافت کنید. این بسته به همراه کدهای اصلی SQLite است که باید در کنار افزونههای آن قرار گیرند تا بتوان این افزونهها را کامپایل کرد. بنابراین پس از دریافت بستهی amalgamation و گشودن آن، فایل spellfix.c را به داخل پوشهی آن کپی کنید:
اکنون نوبت به کامپایل فایل spellfix.c و تبدیل آن به یک dll است تا بتوان آنرا به صورت یک افزونه در برنامه بارگذاری کرد. برای این منظور از هر کامپایلر ++C ای میتوانید استفاده کنید. برای نمونه به آدرس http://www.codeblocks.org/downloads/binaries مراجعه کرده و بستهی codeblocks-20.03mingw-setup.exe را دریافت کنید (بستهای که به همراه mingw است). پس از نصب آن، در مسیر C:\Program Files (x86)\CodeBlocks\MinGW\bin میتوانید کامپایلر چندسکویی gcc را مشاهده کنید. توسط آن میتوان با اجرای دستور زیر، سبب تولید فایل spellfix1.dll شد:
"C:\Program Files (x86)\CodeBlocks\MinGW\bin\gcc.exe" -g -shared -fPIC -Wall D:\path\to\sqlite-amalgamation-3310100\spellfix.c -o spellfix1.dll
روش معرفی افزونههای SQLite به Microsoft.Data.Sqlite
EF Core، از بستهی Microsoft.Data.Sqlite در پشت صحنه برای کار با SQLite استفاده میکند و در اینجا هم برای معرفی افزونهی کامپایل شده، باید ابتدا آنرا به اتصال برقرار شده، معرفی کرد. خود Sqlite در ویندوز، افزونههایش را بر اساس معرفی مستقیم مسیر فایل dll آنها بارگذاری نمیکند. بلکه path ویندوز را برای جستجوی آنها بررسی کرده و در صورتیکه فایل dll ای را افزونه تشخیص داد، آنرا بارگذاری میکند. بنابراین یا باید به صورت دستی مسیر فایل dll تولید شده را به متغیر محیطی path ویندوز اضافه کرد و یا میتوان توسط قطعه کد زیر، آنرا به صورت پویایی معرفی کرد:
using System; using System.Collections.Generic; using System.IO; using System.Runtime.InteropServices; namespace EFCoreSQLiteFTS.DataLayer { public static class LoadSqliteExtensions { public static void AddToSystemPath(string extensionsDirectory) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new NotSupportedException("Modifying the path at runtime only works on Windows. On Linux and Mac, set LD_LIBRARY_PATH or DYLD_LIBRARY_PATH before running the app."); } var path = new HashSet<string>(Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator)); if (path.Add(extensionsDirectory)) { Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, path)); } } } }
در ادامه پیش از معرفی services.AddDbContext، باید مسیر پوشهی افزونهها را ثبت کرد و سپس UseSqlite را به همراه اتصالی استفاده کرد که توسط متد LoadExtension آن، افزونهی spellfix1 به آن معرفی شدهاست:
LoadSqliteExtensions.AddToSystemPath("path to .dll file"); services.AddDbContext<ApplicationDbContext>((serviceProvider, optionsBuilder) => { var connection = new SqliteConnection(connectionString); connection.Open(); connection.LoadExtension("spellfix1"); // Passing in an already open connection will keep the connection open between requests. optionsBuilder.UseSqlite(connection); });
ایجاد جداول ویژهی spell fix در برنامه
در قسمت اول، با متد createFtsTables آشنا شدیم. اکنون این متد را برای ایجاد جداول کمکی مرتبط با افزونهی spell fix به صورت زیر تکمیل میکنیم:
private static void createFtsTables(ApplicationDbContext context) { // For SQLite FTS // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too. context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS"" USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");"); // 'SQLite Error 1: 'no such module: spellfix1'.' --> must be loaded ... // EditCost for unicode support context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_Vocab USING fts5vocab('Chapters_FTS', 'row');"); context.Database.ExecuteSqlRaw("CREATE TABLE IF NOT EXISTS Chapters_FTS_SpellFix_EditCost(iLang INT, cFrom TEXT, cTo TEXT, iCost INT);"); context.Database.ExecuteSqlRaw("CREATE VIRTUAL TABLE IF NOT EXISTS Chapters_FTS_SpellFix USING spellfix1(edit_cost_table=Chapters_FTS_SpellFix_EditCost);"); }
- همانطور که مشاهده میکنید، ابتدا بر اساس Chapters_FTS یا همان جدول مجازی FTS برنامه، یک جدول مجازی از نوع fts5vocab ایجاد میشود. کار آن استخراج توکنهای FTS و آماده سازی آنها برای استفاده در غلط یاب املایی هستند.
- سپس جدول ویژهی EditCost را مشاهده میکنید. نام آن مهم نیست، اما ساختار آن باید دقیقا به همین صورت باشد. اگر این جدول اختیاری را تهیه کنیم، الگوریتم spellfix1 به utf8 سوئیچ خواهد کرد و برای پردازش متون یونیکد، بدون مشکل کار میکند. بدون آن، جستجوهای فارسی نتایج مطلوبی را به همراه نخواهند داشت.
- در آخر جدول مجازی مرتبط با spellfix1 که از جدول cost_table معرفی شده استفاده میکند، ایجاد شدهاست.
اجرای این دستورات، جداول زیر را ایجاد میکنند (که ساختار آنها استاندارد است و باید مطابق فرمولهای مستندات آنها باشد):
به روز رسانی جدول واژه نامهی غلط یابی برنامه
آخرین جدولی را که ایجاد کردیم، Chapters_FTS_SpellFix است که اطلاعات خودش را از Chapters_FTS_Vocab دریافت میکند:
هر بار که بانک اطلاعاتی را به روز میکنیم، نیاز است اطلاعات این جدول را نیز توسط دستور زیر به روز کرد:
database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS_SpellFix(word, rank) SELECT term, cnt FROM Chapters_FTS_Vocab WHERE term not in (SELECT word from Chapters_FTS_SpellFix_vocab)");
database.ExecuteSqlRaw("INSERT INTO Chapters_FTS_SpellFix(command) VALUES(\"reset\");");
کوئری گرفتن از جدول مجازی Chapters_FTS_SpellFix
تا اینجا افزونهی spellfix1 را کامپایل و به سیستم معرفی کردیم. سپس جداول واژه نامهی آنرا نیز تشکیل دادیم، اکنون نوبت به کوئری گرفتن از آن است. به همین جهت یک موجودیت بدون کلید دیگر را بر اساس ساختار خروجی کوئریهای آن ایجاد کرده:
namespace EFCoreSQLiteFTS.Entities { public class SpellCheck { public string Word { get; set; } public decimal Rank { get; set; } public decimal Distance { get; set; } public decimal Score { get; set; } public decimal Matchlen { get; set; } } }
namespace EFCoreSQLiteFTS.DataLayer { public class ApplicationDbContext : DbContext { //... protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity<SpellCheck>().HasNoKey().ToView(null); } //... } }
در آخر، کوئری گرفتن از این جدول، ساختار زیر را دارد:
foreach (var item in context.Set<SpellCheck>().FromSqlRaw( @"SELECT word, rank, distance, score, matchlen FROM Chapters_FTS_SpellFix WHERE word MATCH {0} and top=6", "فارشی")) { Console.WriteLine($"Word: {item.Word}"); Console.WriteLine($"Distance: {item.Distance}"); }
top=6 در این کوئری خاص یعنی 6 رکورد را بازگشت بده.
یک نکته: اگر میخواهید کوئری فوق را توسط برنامهی «DB Browser for SQLite» اجرا کنید، باید از منوی tools آن، گزینهی load extension را انتخاب کرده و فایل dll افزونه را به برنامه معرفی کنید.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
.
تولید یک پرووایدر منابع دیتابیسی - بخش سوم
برای پیادهسازی ویژگی بهروزرسانی ورودیهای منابع در زمان اجرا راهحلهای مخنلفی ممکن است به ذهن برنامهنویس خطور کند که هر کدام معایب و مزایای خودش را دارد. اما درنهایت بسته به شرایط موجود انتخاب روش مناسب برعهده خود برنامهنویس است.
مثلا برای پرووایدر سفارشی دیتابیسی تهیهشده در مطالب قبلی، تنها کافی است ابزاری تهیه شود تا به کاربران اجازه بهروزرسانی مقادیر موردنظرشان در دیتابیس را بدهد که کاری بسیار ساده است. بدین ترتیب بهروزرسانی این مقادیر در زمان اجرا کاری بسیار ابتدایی به نظر میرسد. اما در قسمت قبل نشان داده شد که برای بالا بردن بازدهی بهتر است که مقادیر موجود در دیتابیس در حافظه سرور کش شوند. استراتژی اولیه و سادهای نیز برای نحوه پیادهسازی این فرایند کشینگ ارائه شد. بنابراین باید امکاناتی فراهم شود تا درصورت تغییر مقادیر کششده در سمت دیتابیس، برنامه از این تغییرات آگاه شده و نسبت به بهروزرسانی این مقادیر در متغیر کشینگ اقدامات لازم را انجام دهد.
اما همانطور که در قسمت قبل نیز اشاره شد، نکتهای که باید درنظر داشت این است که مدیریت تمامی نمونههای تولیدشده از کلاسهای موردبحث کاملا برعهده ASP.NET است، بنابراین دسترسی مستقیمی به این نمونهها در بیرون و در زمان اجرا وجود ندارد تا این ویژگی را بتوان در مورد آنها پیاده کرد.
یکی از روشهای موجود برای حل این مشکل این است که مکانیزمی پیاده شود تا بتوان به تمامی نمونههای تولیدی از کلاس DbResourceManager در بیرون از محیط سیستم مدیریت منابع ASP.NET دسترسی داشت. مثلا یک کلاس حاول متغیری استاتیک جهت ذخیره نمونههای تولیدی از کلاس DbResourceManager، به کتابخانه خود اضافه کرد تا با استفاده از یکسری امکانات بتوان این نمونههای تولیدی را از تغییرات رخداده در سمت دیتابیس آگاه کرد. در این قسمت پیادهسازی این راهحل شرح داده میشود.
.
نکته: قبل از هرچیز برای مناسب شدن طراحی کتابخانه تولیدی و افزایش امنیت آن بهتر است تا سطح دسترسی تمامی کلاسهای پیادهسازی شده تا این مرحله به internal تغییر کند. ازآنجاکه سیستم مدیریت منابع ASP.NET از ریفلکشن برای تولید نمونههای موردنیاز خود استفاده میکند، بنابراین این تغییر تاثیری بر روند کاری آن نخواهد گذاشت.
.
نکته: با توجه به شرایط خاص موجود، ممکن است نامهای استفاده شده برای کلاسهای این کتابخانه کمی گیجکننده باشد. پس با دقت بیشتری به مطلب توجه کنید.
.
پیادهسازی امکان پاکسازی مقادیر کششده
برای اینکار باید تغییراتی در کلاس DbResourceManager داده شود تا بتوان این کلاس را از تغییرات بوجود آمده آگاه ساخت. روشی که من برای این کار درنظر گرفتم استفاده از یک اینترفیس حاوی اعضای موردنیاز برای پیادهسازی این امکان است تا مدیریت این ویژگی در ادامه راحتتر شود.
.
اینترفیس IDbCachedResourceManager
این اینترفیس به صورت زیر تعریف شده است:
namespace DbResourceProvider { internal interface IDbCachedResourceManager { string ResourceName { get; } void ClearAll(); void Clear(string culture); void Clear(string culture, string resourceKey); } }
در پراپرتی فقط خواندنی ResourceName نام منبع کش شده ذخیره خواهد شد.
متد ClearAll برای پاکسازی تمامی ورودیهای کششده استفاده میشود.
متدهای Clear برای پاکسازی ورودیهای کششده یک کالچر به خصوص و یا یک ورودی خاص استفاده میشود.
با استفاده از این اینترفیس، پیادهسازی کلاس DbResourceManager به صورت زیر تغییر میکند:
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { internal class DbResourceManager : IDbCachedResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { ... } private object GetCachedObject(string resourceKey, string cultureName) { ... } #region Implementation of IDbCachedResourceManager public string ResourceName { get { return _resourceName; } } public void ClearAll() { lock (this) { _resourceCacheByCulture.Clear(); } } public void Clear(string culture) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Clear(); } } public void Clear(string culture, string resourceKey) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(culture)) return; _resourceCacheByCulture[culture].Remove(resourceKey); } } #endregion } }
اعضای اینترفیس IDbCachedResourceManager به صورت مناسبی در کد بالا پیادهسازی شدند. در تمام این پیادهسازیها مقادیر مربوطه از درون متغیر کشینگ پاک میشوند تا پس از اولین درخواست، بلافاصله از دیتابیس خوانده شوند. برای جلوگیری از دسترسی همزمان نیز از بلاک lock استفاده شده است.
برای استفاده از این امکانات جدید همانطور که در بالا نیز اشاره شد باید بتوان نمونههای تولیدی از کلاس DbResourceManager توسط ASP.NET درون متغیری استاتیک ذخیره شوند. برای اینکار از کلاس جدیدی با عنوان DbResourceCacheManager استفاده میشود که برخلاف تمام کلاسهای تعریفشده تا اینجا با سطح دسترسی public تعریف میشود.
کلاس DbResourceCacheManager
مدیریت نمونههای تولیدی از کلاس DbResourceManager در این کلاس انجام میشود. این کلاس پیادهسازی سادهای بهصورت زیر دارد:
using System.Collections.Generic; using System.Linq; namespace DbResourceProvider { public static class DbResourceCacheManager { internal static List<IDbCachedResourceManager> ResourceManagers { get; private set; } static DbResourceCacheManager() { ResourceManagers = new List<IDbCachedResourceManager>(); } public static void ClearAll() { ResourceManagers.ForEach(r => r.ClearAll()); } public static void Clear(string resourceName) { GetResouceManagers(resourceName).ForEach(r => r.ClearAll()); } public static void Clear(string resourceName, string culture) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture)); } public static void Clear(string resourceName, string culture, string resourceKey) { GetResouceManagers(resourceName).ForEach(r => r.Clear(culture, resourceKey)); } private static List<IDbCachedResourceManager> GetResouceManagers(string resourceName) { return ResourceManagers.Where(r => r.ResourceName.ToLower() == resourceName.ToLower()).ToList(); } } }
ازآنجاکه نیازی به تولید نمونه ای از این کلاس وجود ندارد، این کلاس به صورت استاتیک تعریف شده است. بنابراین تمام اعضای درون آن نیز استاتیک هستند.
از پراپرتی ResourceManagers برای نگهداری لیستی از نمونههای تولیدی از کلاس DbResourceManager استفاده میشود. این پراپرتی از نوع <List<IDbCachedResourceManager تعریف شده است و برای جلوگیری از دسترسی بیرونی، سطح دسترسی آن internal درنظر گرفته شده است.
در کانستراکتور استاتیک این کلاس (اطلاعات بیشتر درباره static constructor در اینجا) این پراپرتی با مقداردهی به یک نمونه تازه از لیست، اصطلاحا initialize میشود.
سایر متدها نیز برای فراخوانی متدهای موجود در اینترفیس IDbCachedResourceManager پیادهسازی شدهاند. تمامی این متدها دارای سطح دسترسی public هستند. همانطور که میبینید از خاصیت ResourceName برای مشخصکردن نمونه موردنظر استفاده شده است که دلیل آن در قسمت قبل شرح داده شده است.
دقت کنید که برای اطمینان از انتخاب درست همه موارد موجود در شرط انتخاب نمونه موردنظر در متد GetResouceManagers از متد ToLower برای هر دو سمت شرط استفاده شده است.
.
نکته مهم: درباره علت برگشت یک لیست از متد انتخاب نمونه موردنظر از کلاس DbResourceManager در کد بالا (یعنی متد GetResouceManagers) باید نکتهای اشاره شود. در قسمت قبل عنوان شد که سیستم مدیریت منابع ASP.NET نمونههای تولیدی از پرووایدرهای منابع را به ازای هر منبع کش میکند. اما یک نکته بسیار مهم که باید به آن توجه کرد این است که این کش برای «عبارات بومیسازی ضمنی» و نیز «متد مربوط به منابع محلی» موجود در کلاس HttpContext و یا نمونه مشابه آن در کلاس TemplateControl (همان متد GetLocalResourceObject که درباره این متدها در قسمت سوم این سری شرح داده شده است) از یکدیگر جدا هستند و استفاده از هریک از این دو روش موجب تولید یک نمونه مجزا از پرووایدر مربوطه میشود که متاسفانه کنترل آن از دست برنامه نویس خارج است. دقت کنید که این اتفاق برای منابع کلی رخ نمیدهد.
بنابراین برای پاک کردن مناسب ورودیهای کششده در کلاس فوق به جای استفاده از متد Single در انتخاب نمونه موردنظر از کلاس DbResourceManager (در متد GetResouceManagers) از متد Where استفاده شده و یک لیست برگشت داده میشود. چون با توجه به توضیح بالا امکان وجود دو نمونه DbResourceManager از یک منبع درخواستی محلی در لیست نمونههای نگهداری شده در این کلاس وجود دارد.
.
افزودن نمونهها به کلاس DbResourceCacheManager
برای نگهداری نمونههای تولید شده از DbResourceManager، باید در یک قسمت مناسب این نمونهها را به لیست مربوطه در کلاس DbResourceCacheManager اضافه کرد. بهترین مکان برای انجام این عمل در کلاس پایه BaseDbResourceProvider است که درخواست تولید نمونه را در متد EnsureResourceManager درصورت نال بودن آن میدهد. بنابراین این متد را به صورت زیر تغییر میدهیم:
private void EnsureResourceManager() { if (_resourceManager != null) return; { _resourceManager = CreateResourceManager(); DbResourceCacheManager.ResourceManagers.Add(_resourceManager); } }
تا اینجا کار پیادهسازی امکان مدیریت مقادیر کششده در کتابخانه تولیدی به پایان رسیده است.
.
استفاده از کلاس DbResourceCacheManager
پس از پیادهسازی تمامی موارد لازم، حالتی را درنظر بگیرید که مقادیر ورودیهای تعریف شده در منبع "dir1/page1.aspx" تغییر کرده است. بنابراین برای بروزرسانی مقادیر کششده کافی است تا از کدی مثل کد زیر استفاده شود:
DbResourceCacheManager.Clear("dir1/page1.aspx");
کد بالا کل ورودیهای کششده برای منبع "dir1/page1.aspx" را پاک میکند. برای پاک کردن کالچر یا یک ورودی خاص نیز میتوان از کدهایی مشابه زیر استفاده کرد:
DbResourceCacheManager.Clear("Default.aspx", "en-US"); DbResourceCacheManager.Clear("GlobalTexts", "en-US", "Yes");
.
دریافت کد پروژه
کد کامل پروژه DbResourceProvider به همراه مثال و اسکریپتهای دیتابیسی مربوطه از لینک زیر قابل دریافت است:
برای استفاده از این مثال ابتدا باید کتابخانه Entity Framework (با نام EntityFramework.dll) را مثلا از طریق نوگت دریافت کنید. نسخهای که من در این مثال استفاده کردم نسخه 4.4 با حجم حدود 1 مگابایت است.
نکته: در این کد یک بهبود جزئی اما مهم در کلاس ResourceData اعمال شده است. در قسمت سوم این سری، اشاره شد که نام ورودیهای منابع Case Sensitive نیست. بنابراین برای پیادهسازی این ویژگی، متدهای این کلاس باید به صورت زیر تغییر کنند:
public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name.ToLower() == _resourceName.ToLower() && r.Key.ToLower() == resourceKey.ToLower() && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name.ToLower() == _resourceName.ToLower() && r.Culture == culture).ToList(); } }
.
در آینده...
در ادامه مطالب، بحث تهیه پرووایدر سفارشی فایلهای resx. برای پیادهسازی امکان بهروزرسانی در زمان اجرا ارائه خواهد شد. بعد از پایان تهیه این پرووایدر سفارشی، این سری مطالب با ارائه نکات استفاده از این پرووایدرها در ASP.NET MVC پایان خواهد یافت.
.
منابع
دوره آموزشی Blazor
Welcome to this short introduction to Blazor! This new Microsoft framework uses a unique approach to leverage your existing C# and .NET skills to create single-page applications running in web browsers. The technology that makes this possible is called WebAssembly, an open standard supported directly by current browsers on desktop and mobile platforms. You write C# and Razor code instead of JavaScript, and the compiled app runs natively on the client.
Sample Source Code: https://github.com/DevExpress/blazor-training-samples
[HttpPost,AjaxOnly, ValidateAntiForgeryToken] public virtual JsonResult Create(AddDeviceGroupViewModel deviceGroupViewModel) { if (ModelState.IsNotValid()) { Response.StatusCode = (int)HttpStatusCode.BadRequest; return Json(new { success = false, message = ModelState.FirstErrorMessage(), notificationType = NotificationType.Error }, JsonRequestBehavior.AllowGet); } var result = _deviceGroupService.Add(deviceGroupViewModel); // other codes }
@using (Ajax.BeginForm(MVC.Admin.DeviceGroup.Create(), new AjaxOptions { HttpMethod = "POST", OnSuccess = "saveAjaxForm", OnFailure = "SaveFailure" })) { // form content }
function SaveFailure(data) { $("button[type=submit]").prop('disabled', false); var result = $.parseJSON(data.responseText); showMessage(result.message, result.notificationType); }
مشکل به وجود آمده حاصل اضافه شدن تگهای زیر به وب کانفیگ بود :
<httpErrors errorMode="Custom"> <remove statusCode="404"/> <error statusCode="404" path="/Error/NotFound" responseMode="ExecuteURL"/> </httpErrors>
Response.StatusCode = (int)HttpStatusCode.BadRequest;
و در خروجی تنها مقداری که به سمت کاربر برگشت داده میشد، مقدار BadRequest بود و خط زیر باعث خطا و توقف برنامه میشد:
var result = $.parseJSON(data.responseText);
همچنین در صورتیکه قصد داشتید تگهای فوق را در web.config داشته باشید (جهت هندل کردن صفحات پیدا نشده) میتوانید از مقدار دهی TrySkipIISCustomError با true این مشکل را رفع کنید.
Response.TrySkipIisCustomErrors = true;
روشی دیگر:
<system.webServer> <httpErrors errorMode="DetailedLocalOnly" existingResponse="PassThrough" />
- آدرسی که کار میکند، یعنی جزو مسیریابی سیستم ثبت شدهاست.
- برای فعال سازی CORS این مراحل باید طی شوند:
در فایل WebApiConfig.cs، در ابتدای متد Register این کدها باید اضافه شوند:
var cors = new EnableCorsAttribute("*", "*", "*"); config.EnableCors(cors); config.MessageHandlers.Add(new PreflightRequestsHandler());
public class PreflightRequestsHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Headers.Contains("Origin") && request.Method.Method == "OPTIONS") { var response = new HttpResponseMessage {StatusCode = HttpStatusCode.OK}; response.Headers.Add("Access-Control-Allow-Origin", "*"); response.Headers.Add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); response.Headers.Add("Access-Control-Allow-Methods", "*"); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } return base.SendAsync(request, cancellationToken); } }
jQuery.support.cors = true;
using Microsoft.AspNetCore.Mvc; namespace Core3xWebApi.Controllers { [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { [HttpGet] public ActionResult<string> Get() { return null; } } }
همانطور که مشاهده میکنید، هیچ خروجی تولید نشده و تنها به تنظیم status-code مساوی 204 که به معنای no content است، اکتفا کردهاست.
چرا از کشن متدهای ASP.NET Core نمیتوان خروجی نال گرفت؟
فرمت خروجی اکشن متدها در ASP.NET Core بر اساس output formatterهای متفاوتی مانند JSON ،XML و غیره، تعیین میشود. اما زمانیکه به نال میرسد، از یک output formatter خاص به نام HttpNoContentOutputFormatter کمک میگیرد. کار آن تبدیل null، به خروجی مخصوصی است که توضیح داده شد. به این معنا که هیچگاه خروجی نال برنامه، برای مثال به نحو متداولی تبدیل به یک خروجی استاندارد JSON نمیشود و این مساله برای Http Clientهای مختلفی که منتظر یک خروجی استاندارد هستند (مانند انواع برنامههای تک صفحهای وب)، عموما مساله ساز است؛ چون هر status-code دیگری بجز 200 را به صورت بروز یک خطا تفسیر میکنند که در حالت فوق، فقط به معنای نبود اطلاعات است و نه بروز خطایی.
چگونه خروجی نال را به همان شکلی که هست بازگشت دهیم؟
HttpNoContentOutputFormatter به همراه خاصیت TreatNullValueAsNoContent نیز هست که اگر در ابتدای تنظیمات برنامه به false تنظیم شود، دیگر مقادیر نال را به خروجی no content تبدیل نمیکند:
namespace Core3xWebApi { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddControllers(options => { // remove formatter that turns nulls into 204 - No Content responses // this formatter breaks SPA's Http response JSON parsing options.OutputFormatters.RemoveType<HttpNoContentOutputFormatter>(); options.OutputFormatters.Insert(0, new HttpNoContentOutputFormatter { TreatNullValueAsNoContent = false }); }); }
ایجاد یک Repository در پروژه برای دستورات EF
using System; using System.Collections; using System.Linq; namespace Framework.Model { public interface IContext { T Get<T>(Func<T, bool> prediction) where T : class; IEnumerable List<T>(Func<T, bool> prediction) where T : class; void Insert<T>(T entity) where T : class; int Save(); } }
using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Linq; using System.Text; namespace Framework.Model { public class Context : IContext { private readonly DbContext _dbContext; public Context(DbContext context) { _dbContext = context; } public T Get<T>(Func<T,bool> prediction) where T : class { var dbSet = _dbContext.Set<T>(); if (dbSet!= null) return dbSet.Single(prediction); throw new Exception(); } public void Insert<T>(T entity) where T : class { var dbSet = _dbContext.Set<T>(); if (dbSet != null) { _dbContext.Entry(entity).State = EntityState.Added; } } public int Save() { return _dbContext.SaveChanges(); } IEnumerable IContext.List<T>(Func<T, bool> prediction) { var dbSet = _dbContext.Set<T>(); if (dbSet != null) return dbSet.Where(prediction).ToList(); throw new Exception(); } } }
using System.Data.Entity; using DataModel; namespace Model { public class EFContext : DbContext { public EFContext(string db): base(db) { } public DbSet<Product> Products { get; set; } } }
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Text; namespace Model { public class Context : Framework.Model.Context { public Context(string db): base(new EFContext(db)) { } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Biz { public class Context : Model.Context { public Context(string db) : base(db) { } } }
using System.Web.Mvc; using Framework.Model; namespace ProductionRepository.Controllers { public class BaseController : Controller { public IContext DataContext { get; set; } public BaseController() { DataContext = new Biz.Context(System.Configuration.ConfigurationManager.ConnectionStrings["Database"].ConnectionString); } } }
using System.Web.Mvc; using DataModel; using System.Collections.Generic; namespace ProductionRepository.Controllers { public class ProductController : BaseController { public ActionResult Index() { var x = DataContext.List<Product>(s => s.Name != null); return View(x); } } }
using NUnit.Framework; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Mvc; namespace TestUnit { [TestFixture] public class Test { [Test] public void IndexShouldListProduct() { var repo = new Moq.Mock<Framework.Model.IContext>(); var products = new List<DataModel.Product>(); products.Add(new DataModel.Product { Id = 1, Name = "asdasdasd" }); products.Add(new DataModel.Product { Id = 2, Name = "adaqwe" }); products.Add(new DataModel.Product { Id = 4, Name = "qewqw" }); products.Add(new DataModel.Product { Id = 5, Name = "qwe" }); repo.Setup(x => x.List<DataModel.Product>(p => p.Name != null)).Returns(products.AsEnumerable()); var controller = new ProductionRepository.Controllers.ProductController(); controller.DataContext = repo.Object; var result = controller.Index() as ViewResult; var model = result.Model as List<DataModel.Product>; Assert.AreEqual(4, model.Count); Assert.AreEqual("", result.ViewName); } } }
اولین نکتهای که باید بدانید این است که هر نوع دادهای که شما در دات نت دارید و تعریف میکنید، از والدی به نام System.Object مشتق میشود. برای مثال وقتی شما کلاسی را تعریف میکنید، چه ضمنی و چه صریح، کلاس شما از System.Object ارث بری خواهد کرد.
class DotnetTips { .... }
class DotnetTips:System.Object { ... }
Equals | اگر هر دو شیء مقادیر یکسانی داشته باشند، True باز میگرداند. در فصل پنجم با این مورد بیشتر آشنا میشویم. |
GetHashCode | بر اساس مقادیر این شیء، یک کد هش شده میسازد. اگر شیء قرار است مقدار هش آن در جداول هش، مثل Dictionary یا HashSet به عنوان کلید به کار رود بهتر است این متد رونویسی شود. متاسفانه این متد در شیء Object تعریف شده است و از آنجا که بیشتر نوعها هیچ وقت به عنوان یک کلید استفاده نمیشوند؛ بهتر بود در این رابطه به یک اینترفیس منتقل میشد. در فصل 5 بیشتر در این مورد بحث خواهد شد (مطالب مرتبط در اینباره + + + ). |
ToString | پیاده سازی پیشفرض آن اینست که این متد با اجرای کدthis.GetType().FullName
نام نوع را برمیگرداند. ولی برای بعضی نوعها چون Boolean,int32 این متد، رونویسی شده و مقدار مربوطه را به طور رشتهای باز میگرداند. در بعضی موارد هم این متد برای استفادههایی چون دیباگ برنامه هم رونویسی میشود. توجه داشته باشید که که نسبت به CultureInfo ترد مربوطه، واکنش نشان میدهد که در فصل 14 آن را بررسی میکنیم. |
GetType | یک نمونه از شیء مورد نظر را برمیگرداند که با استفاده از ریفلکشن میتوان به اطلاعات متادیتای آن شیء دسترسی پیدا کرد. در مورد ریفلکشن در فصل 23 صحبت خواهد شد. |
Protected Methods
MemberwiseClone | این متد از شیء جاری یک Shallow copy (+ )گرفته و فیلدهای غیر ایستای آن را همانند شیء جاری پر میکند. اگر فیلدهای غیر ایستا از نوع Value باشند که کپی بیت به بیت صورت خواهد گرفت. ولی اگر فیلدی از نوع Reference باشد، از همان نوع Ref میباشند؛ ولی خود شیء اصلی، یک شیء جدید به حساب میآید و به شیء سازندهاش ارتباطی ندارد. |
Finalize | یک متد Virtual است و زمانی صدا زده میشود که GC قصد نابودسازی آن شیء را داشته باشد. این متد برای زمانی نیاز است که شما قصد دارید قبل از نابودسازی، کاری را انجام دهید. در فصل 21 در این مورد بیشتر صحبت میکنیم. |
همانطور که میدانید، همهی اشیا نیاز دارند تا با استفاده از کلمهی new، در حافظه تعریف شوند:
DotnetTips dotnettips=new DotnetTips("i am constructor");
اتفاقاتی که با استفاده از عملگر new رخ میدهد به شرح زیر است:
- اولین کاری که CLR انجام میدهد، محاسبهی مقدار حافظه یا تعداد بایتهای فیلدهای شیء مربوطه است و همچنین اشیایی که از آنها مشتق شده است؛ مثل System.Object که همیشه وجود دارد. هر شیءایی که بر روی حافظهی Heap قرار میگیرد، به یک سری اعضای اضافه هم نیاز دارد که Type Object Pointer و Sync Block Index نامیده میشوند و CLR از آنها برای مدیریت حافظه استفاده میکند. تعداد بایتهای این اعضای اضافه هم با تعداد بایتهای شیء مدنظر جمع میشوند.
- طبق تعداد بایتهای به دست آمده، یک مقدار از حافظهی Heap دریافت میشود.
- دو عضو اضافه که در بند شماره یک به آن اشاره کردیم، آماده سازی میشوند.
- سازندهی شیء صدا زده میشود. سازنده بر اساس نوع ورودی شما انتخاب میشود و در صورت ارجاع به سازندههای والد، ابتدا سازندههای والد صدا زده میشوند و بعد از اینکه همهی سازندهها صدا زده شدند، سازندهی System.Object صدا زده میشود که هیچ کدی ندارد؛ ولی به هر حال عمل Return آن انجام میشود.
بعد از انجام همهی موارد بالا، یک اشاره گر به متغیر شما (در اینجا dotnettips) برگشت داده میشود.