آیا از روش غیر استاتیک که معرفی کردید برای این سناریو میشود استفاده کرد
ref
keyword معرفی شد. EF Code First #11
دو خبر در مورد برنامه Reflector
کار کردن با مسیریابی برای یک پروژه ساده ، نیاز به طراحی پیچیده ندارد. مسیریابی پیش فرض موجود در فایل RoutConfig.cs برای کارهای ابتدایی کافیست. اما اگر کمی کار پیچیده شود و صفحات مختلفی با منطقهای متفاوتی ایجاد کنیم، ممکن است با مشکل روبرو شویم. در MVC5 به کمک دخالت ویژگیها در مسیریابی، کار ساده شده است اما در MVC4 و قبل از آن چه باید کرد؟ پیش از بسط مساله، ابتدا این سوال را پاسخ میدهیم که چگونه صفحهی start پروژه انتخاب میشود؟
مسیریابی پیش فرض یک پروژه MVC به شکل زیر است :
routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
وقتی یک پروژه MVC را بررسی کنید، مشاهده میکنید که در شاخهی اصلی آن، فایل index یا default وجود ندارد و اصولا منطق کار با اکشنها و صدا زدن آنها و دیدن پاسخها توسط View، این اتفاق را توجیه میکند. پس وقتی درخواستی به سمت شاخهی اصلی ما فرستاده میشود، مسیریابی وارد عمل میشود. وقتی پروژه را اجرا میکنید، متد RegisterRoutes از شیءایی که از کلاس RoutingConfig.cs ساخته شده (به فایل global.asax نگاه کنید) فراخوانی میشود و شیء Routes از " قالب آدرس " هایی که ما تعیین کرده ایم پُر میشود. بخشی از فایل RoutConfig.cs را در تصویر زیر میبینیم.
به Url کد فوق نگاه کنید که در حقیقت آدرسی نسبی است. آدرس ما به طور کامل به شکل زیر قابل تعریف است:
http://mydomain.com/{controller}/{action}/{id}
واضح است که درخواست اولیه به سایت ما دارای بخشهای controller و action و بخش اختیاری id نیست. پس به شکل پیشفرض بر اساس آنچه در جلوی خصوصیت defaults نوشته شده است، فراخوانی خواهد شد. یعنی اکشن Index از داخل کنترلر Home صدا زده میشود و چون id نداریم، هیچ نوع id به متد (اکشن) index ارسال نخواهد شد.
یک روتینگ دیگر به شکل زیر در بالای کد فوق اضافه کنید :
routes.MapRoute( name: "Default1", url: "{controller}/{action}/{id}", defaults: new { controller = "News", action = "Index", id = UrlParameter.Optional } );
حتما متوجه شدید که اینبار با اجرای پروژه، متد(اکشن) Index از کنترلر News فراخوانی خواهد شد. در حقیقت اولین روتینگ ساخته شده همان چیزیست که Asp.NET MVC پس از دریافت ریکوئست به آن رجوع میکند. دقت کنید که مقدار name در دو MapRouting فوق متفاوت است.
اما این اسمها(name ها) به چه دردی میخورند؟
قبل از توضیح در این مورد، default1 را از پروژه حذف میکنیم و با همان MapRouting پیش فرض به کار خود ادامه میدهیم. ابتدا مروری بر عملکرد MapRouting انجام میدهیم.
همانطور که میدانید برای ایجاد یک لینک از طریق ویوی Razor از چیزی شبیه دستور زیر میتوانیم استفاده کنیم:
<a href="@Url.Action("post", "News", new { id = @item.PostName }, null)"> @item.PostTitle </a>
فرض کنید یک سایت خبری داریم و گروههای مختلف آن شامل مطالب متفاوتی هستند. کنترلری به نام News ایجاد کردهایم، که دارای یک اکشن به نام post است که نام مطلب را دریافت کرده و یک View به ما برمیگرداند. بخش هایی از صفحه اول را در تصاویر زیر میبینید. تصویر آخر، بخش پایین صفحه اخبار است، که بوسیله لینک (لینک های) موجود صفحات جابجا میشوند.
وقتی کد فوق در یک صفحه فراخوانی میشود، کالکشن Routes بررسی میشود و لینک مورد نظر، با توجه به ورودیهای Url.Action ساخته میشود. ما یک MapRouting به نام Default در کالکشن Routes داریم، پس Url.Action لینک زیر را میسازد:
http://mydomain.com/news/post/نام-مقاله
استفاده از MapRouting مثل یک جعبه است که ورودی آن مقادیر موجود در Url.Action میباشد و آنچه به ما بر میگرداند یک آدرس برای فراخوانی اکشن مورد نیاز است. (آخرین پارامتر، null قرار داده شده که برای تعیین نوع پروتکل استفاده میشود که http یا https میتواند باشد.)
برای فراخوانی صفحهی اخبار (لیست اخبار) میتوانید دستور زیر را بنویسید :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
این دستور تگ anchor را نیز میسازد و به کمک MapRouting آدرسی شبیه آدرس زیر را به ما بر میگرداند. (به تفاوت دو دستور دقت کنید):
<a href="/News">اخبار</a>
واضح است که چون MapRouting یک حالت پیش فرض درونی دارد که دارای اکشن دیفالت index است، پس ActionLink اگر ببیند لینکش در صفحه قرار است به شکل /news/index تعریف شود خود بخود بخش index را حذف میکند.
فرض کنید بعد از کلیک روی لینک فوق، اکشن index ، یک آبجکتِ لیستی با 10 خبر آخر، ایجاد میکند. پس ما میخواهیم قابلیت صفحه بندی را برای لیست اخبار فعال کنیم و در هر صفحه 10 مطلب را نمایش دهیم. مشکل از همینجا آغاز میشود. MapRouting فعلی جوابگوی ما نخواهد بود و آدرس را به شکل زیر نمایش میدهد.
<a href="/News/index?pid=2">صفحه بعد</a>
و آنچه ما در View نوشتهایم چیزی شبیه کد زیر است :
@Html.ActionLink("صفحه بعد", "index", "News", new { pid = …. }, null)
مشکلی در ارجاع به صفحات وجود ندارد و با کلیک روی لینک "صفحه بعد" مقدار عدد 2 به اکشن index ارسال میشود و اگر کد نویسی را برای take و skip کردن لیست، درست انجام شده باشد، نتیجه مورد نظر نمایش داده خواهد شد. اما آدرس فوق آدرس زیبایی نیست. اولین فکری که به ذهن برنامه نویس میرسد، ایجاد یک مسیریابی دیگر است. فکر درستیست؛ اما اگر چند بار دیگر این اتفاق بیفتد و در بخش هایی از برنامه نیاز به روتینگ پیدا کنید و روتینگهای جدید ایجاد کنید متوجه خواهید شد که مدیریت این MapRouting ها کار خسته کننده و طاقت فرسایی خواهد شد، مخصوصا اگر بدانید که فقط مجاز به استفاده از یک پارامتر optional در هر MapRouting هستید! دست شما کاملا بسته است. لینکهای بالای سایت را اصلاح میکنید ولی لینکهای پایین سایت خراب میشوند و بالعکس.
به هر حال MapRouting زیر را به RoutConfig.cs اضافه میکنیم :
اینکار بستگی به پروژهی شما دارد، مپ روتینگ فوق این مزیت را دارد که با مپ روتینگهای دیگر به سختی قاطی میشود! یعنی حتا اگر قبل از مپ روتینگ دیفالت نوشته شود برنامه با مشکل مواجه نخواهد شد، چون اصلا شکل درخواست اولیه به سایت، چیزی شبیه این آدرس نیست. اما خاص بودن آن و همچنین نوع بهره گیری از آن با کمک Action یا ActioLink شاید شما را سردرگم خواهد کند.- اسم این MapRouting ، دیگر Default نیست.routes.MapRoute("PostPaging", "{controller}/{action}/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
- یک پارامتر pid اضافهتر از MapRouting اولی دارد.
- pid به عنوان یک پارامتر اختیاری تعریف شده است، پس "قالب آدرس" بسیار شبیه مپ روتینگ قبلی است.
- مقدار id اختیاری نیست، چون قرار است در آینده بتوانیم گروههای مختلف موجود در بخش اخبار را صفحه بندی کنیم و قرار نیست پشت سر هم MapRouting ایجاد کنیم و کافیست به جای id اسم گروه را بنویسیم. در حالتیکه اسمی از گروه درلینکهایمان نبرده باشیم به شکل پیشفرض all قرار داده میشود که یعنی کل اخبار مد نظر است. (در اکشن مربوطه باید این تصمیمات را لحاظ کنیم)
- حتما این MapRouting را بعد از MapRouting اولیه بنویسید، کمی پیشتر، علت این امر توضیح داده شد و گفته شد اولین چیزی که MVC پس از درخواست ما میبیند به عنوان Routing بررسی میکند (درخواست اولیه) و چون ساختار MapRouting فوق تا اندازه ای شبیه ساختار Default MapRouting است ممکن است با فراخوانی سایت مشکل ایجاد شود.
- میتوانید MapRouting را کمی خاصتر هم بنویسیم :
routes.MapRoute("NewsPaging", "News/index/{id}/{pid}", defaults: new { controller = "News", action = "Index", id = "all", pid = UrlParameter.Optional } );
اما مشکل این MapRouting ها چیست؟
درخواست به سایت آمده و قرار است سایت بارگذاری شود؛ ترتیب زیر در شیء routes ثبت شده است :
در صفحه اول ما لینکی به شکل زیر گذاشته ایم :
@Html.ActionLink("اخبار", "Index", "News", null, new { @class="btn btn-success"})
در صفحه اخبار، لینک "صفحه بعد" وجود دارد ، آیا این لینک به شکل صحیح نمایش داده میشود؟ خیر! نتیجه کار را ببینید :
<a href="/News/index?pid=2">صفحه بعد</a>
به نظر میرسد یک طرح دقیق برای آدرس دهی شاید این مسائل را حل کند ولی نه وقت داریم و نه اعصاب.
1. ساخته شدن لینکها توسط هلپرها (که از مپ روتینگهای ثبت شده ما پیروی میکند)
2. واکنش پروژه به درخواستهای دریافتی و هدایت آن به اکشنهای مورد نظر که کاملا به ترتیب ثبت مپ روتینگها در کالکشن Routes بستگی دارد .
@Html.RouteLink("صفحه بعد", "PostPaging", new { action = "cat", controller = "News", id =. .., pid = ...})
نتیجه برای لینک موجود در صفحه اخبار چیزی شبیه شکل زیر خواهد شد :
<a href="/News/cat/all/2">صفحه بعد</a>
@Html.RouteLink("اخبار", "Default", new { controller = "news" }) @Html.RouteLink("درباره ما", "pages", new RouteValueDictionary(new { controller="Page", action="Index", pagename="درباره-ما"}), new Dictionary<string, Object> { { "data-toggle", "popover" }, { "data-placement", "top" } })
همانطور که میبینید در RoutLink
اولی، اخبار را به کمک MapRouting
با نام default
بازنویسی میکنیم و نتیجه چیزی شبیه کد زیر خواهد شد :
<a href="/News">اخبار</a>
در RoutLink دومی اولا از یک RoutValueDictionary به جای یک آبجکت ساده استفاده کرده ایم و مقادیر را به شکل فوق به کنترلر و اکشن و ...نسبت داده ایم ثانیا برای بخش HTML نیز پراپرتیها را به کمک یک دیکشنری ارسال میکنیم، به خاطر وجود "-" در یکی از خواص، راه دیگری غیر از اینکار نداریم.
اما دقت کنید که از یک MapRouting جدید استفاده کردیم که نامش pages است،
این MapRoutnig را قبل از دیگر Routing ها مینویسیم؟ وسط دو MapRouting قبلی مینویسیم؟ آخر MapRouting ها مینویسیم؟ آیا فرقی میکند؟ اگر سریع بگوییم خــیر! اشتباه کرده ایم. واقعا فرق میکند.
دقت کنید موضوع MapRouting فقط ایجاد یک لینکتر و تمیز نیست؛ RoutLink یک لینک تمیز بر اساس مپ روتینگی که نامش برده شده ایجاد میکند
اما تضمین نمیکند که با کلیک بر روی لینک به هدف برسیم و به خطای 404 برخورد نکنیم! اگر روی لینک کلیک کنید آدرس شروع به تفسیر شدن
میکند و این تفسیر اصلا ربطی به نامی که به RoutLink داده ایم ندارد و ترتیب موجود در کالکشن ایجاد شده در RoutConfig تعیین کننده است.(آبجکت Routes ) اگر MapRouting فوق را در انتهای بقیه بگذاریم صفحه اول لود میشود ولی با کلیک
روی "درباره ما" صفحه پیغام خطا خواهد داد.
باید به یاد
داشته باشیم برای اجرای درخواست (کلیک روی لینک)، آنچه برای ASP.NET MVC اهمیت دارد، ترتیب قرار گیری MapRouting ها در RouteRegister است و
ما به کمک RoutLink تنها
مشکل ساخت لینکها بر اساس قالب MapRouting مورد
نظرمان را حل کردیم و این به ما تضمینی برای هدایت آن لینک به مکان درست را نخواهد
داد.
اگر ترتیب به شکل زیر باشد :
1باشد. درخواست اولیه برای بالا آمدن سایت به مشکل برخورد نمیکند چون همان مپ روتینگ 1 اجرا میشود. اما مشکل فوق به وجود خواهد آمد و خطای 404 با کلیک بر روی "درباره ما" نمایش داده خواهد شد چون با کلیک روی "درباره ما" مپ روتینگ شماره 1 وارد عمل میشود.
2
3
اگر ترتیب به شکل زیر باشد :
3آیا اصلا صفحه اول سالم لود خواهد شد؟ خیر! درخواست نسبی " / " (یا به طور کامل http://mydomain.com ) شماره 3 را به خیر پشت سر میگذارد، چون اصلا چیزی به نام page در آدرس وجود ندارد که از این MapRouting بخواهد پیروی کند. اما در شماره 2 گیر میافتد چون این فرمت را حفظ کرده است :
2
1
"{controller}/{action}/{id}/{pid}"
اگر ترتیب به شکل زیر باشد :
3این همان چیزیست که مد نظر ماست. اولا 1 قبل از 2 است و صفحه اول برای لود شدن به مشکل برخورد نمیکند.
1
2
ثانیا وقتی روی "درباره ما" کلیک میکنیم همان شماره 3 فراخوانی میشود و بقیه مپ روتینگها اعمال نمیشوند.
مطالب پیشین مرتبط با لوسین را در اینجا میتوانید پیگیری کنید. آخرین نگارش آن که تا این تاریخ، 4.8 بتا است، با داتنت(Core) سازگار است و روش برپایی آغازین آن ... تغییرات قابل توجهی داشتهاست که خلاصهی آنها را در این مطلب بررسی خواهیم کرد.
1) بستههای جدید مورد نیاز
برای کار با لوسین جدید، نیاز است حداقل سهبستهی زیر را نصب کنیم تا به امکانات پایهای و کوئری گیریهای پیشرفتهی آن دسترسی داشته باشیم:
<PackageReference Include="Lucene.Net" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.Analysis.Common" Version="4.8.0-beta00016"/> <PackageReference Include="Lucene.Net.QueryParser" Version="4.8.0-beta00016"/>
2) تهیه نگاشتهای لازم
فرض کنید شیء اصلی ما چنین ساختاری را دارد:
public class WhatsNewItemModel { public required int Id { set; get; } public required string OriginalTitle { set; get; } }
مرحلهی بعد کار با لوسین، تبدیل اشیاء سفارشی خود به شیء Document لوسین و برعکس است. به همین جهت به دو مپر برای این کارها نیاز است:
الف) نگاشتگر یک شیء سفارشی، به شیء Document
public static class LuceneDocumentMapper { public static Document MapToLuceneDocument(this WhatsNewItemModel post) { ArgumentNullException.ThrowIfNull(post); return [ new TextField(nameof(WhatsNewItemModel.OriginalTitle), post.OriginalTitle, Field.Store.YES), // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). new StringField(nameof(WhatsNewItemModel.Id), post.Id.ToString(CultureInfo.InvariantCulture), Field.Store.YES), ]; } }
در اینجا یک متدالحاقی را تهیه کردهایم تا شیءای از نوع WhatsNewItemModel ما را به یک شیء Document لوسین، تبدیل کند.
چند نکته در اینجا حائز اهمیت هستند:
- در نگارش جدید لوسین، با اشیاء TextField و StringField جدید سروکار داریم و شیء قدیمی Field نگارشهای قبلی لوسین، منسوخ شده درنظر گرفته میشود.
- زمانی از شیء TextField استفاده میکنیم که قرار است توسط لوسین، تحلیل شده و در جستجوهای پیچیده استفاده شود.
- اگر فقط قرار است، مقداری را در این ایندکس ذخیره کنیم و قصد تحلیل آنها را نداریم و حداکثر یک کوئری سادهی یافتن اصل آنها، مدنظر ما است، باید از اشیاء StringField برای معرفی و نگاشت آنها استفاده کنیم (شبیه به کار با واژههای کلیدی).
- پرچم Field.Store.YES به این معنا است که اصل محتوای تحلیل شده نیز در ایندکس لوسین، درج شود. اگر این پرچم را به NO تنظیم کنیم، فقط تحلیل آن صورت گرفته و نتیجهی آن ذخیره میشود، که برای جستجوها مفید است؛ اما مقدار این فیلد دیگر قابل بازیابی نخواهد بود.
ب) نگاشتگر یک شیء Document لوسین، به یک شیء سفارشی
در زمان کوئری گرفتن از لوسین، خروجی نهایی یک شیء Document آن است که باید به شیء سفارشی مدنظر ما نگاشت شود:
public static class LuceneDocumentMapper { public static LuceneSearchResult MapToLuceneSearchResult(this Document document) { ArgumentNullException.ThrowIfNull(document); return new LuceneSearchResult { Id = document.Get(nameof(WhatsNewItemModel.Id), CultureInfo.InvariantCulture).ToInt(), OriginalTitle = document.Get(nameof(WhatsNewItemModel.OriginalTitle), CultureInfo.InvariantCulture) }; } }
نمونهای از این نگاشت را در متد الحاقی فوق مشاهده میکنید که توسط متد Get شیء Document قابل انجام است. بدیهی است خروجی این متد، یک رشتهاست و در صورت نیاز باید توسط ما کار تبدیلات ثانویه آنها انجام شود.
3) نیاز به یک تحلیلگر مناسب
لوسین برای تولید ایندکسهای جستجوی تمام متنی خود، از یک سری Analyzer استفاده میکنید که اگر سری پیشین مطالب مرتبط را مطالعه کنید، به نمونهی StandardAnalyzer آن خواهید رسید که هنوز هم معتبر و قابل استفادهاست و یا میتوان همانند سایت جاری، از یک LowerCaseHtmlStripAnalyzer استفاده کرد که این کارها را همزمان انجام میدهد:
الف) از یک لیست PersianStopwords.List برای حذف واژههای کم اهمیت زبان فارسی استفاده میکند. برای مثال ما نمیخواهیم که واژهی «ما» را با اهمیت شمرده و ایندکس کند و امثال آن.
ب) LowerCaseFilter را به متون دریافتی اعمال میکند. این کار در پشت صحنهی StandardAnalyzer توکار لوسین هم اعمال میشود. اگر با این موضوع آشنا نباشید، ممکن است در حین کوئری گرفتن، به نتیجهای نرسید! چون متن ارسالی به لوسین را ابتدا باید lower-case کنید و سپس آنرا کوئری بگیرید.
ج) HTMLStripCharFilter توکار لوسین هم به آن اعمال شدهاست. از این جهت که متن مقالات ما به همراه تگهای HTML ای هم هستند. این فیلتر کار حذف کردن آنها را در حین تحلیل، انجام میدهد و دیگر نیازی نیست تا ما خودمان متن ارسالی به لوسین را تمیز کنیم.
نکتهی مهم: این تحلیلگر ویژه، فقط باید به فیلدهایی از نوع TextField اعمال شود. اگر آنرا به StringField ها اعمال کنیم، دیگر قادر به کوئری گرفتن از آنها نخواهیم بود! چون تحلیلگر StringFieldها باید از نوع توکار KeywordAnalyzer ثبت و معرفی شود. این نوع فیلدها، حالت واژههای کلیدی را دارند (به همان صورتی که هست ثبت میشوند) و قرارنیست که توسط لوسین تحلیل ویژهای شوند. به همین جهت برای رسیدن به یک تحلیلگر ترکیبی که بتواند این دو نوع فیلد را با هم پوشش دهد و کار معرفی چندین نوع تحلیلگر را یکجا انجام دهد، نیاز به یک PerFieldAnalyzerWrapper جدید داریم:
_keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { { nameof(WhatsNewItemModel.Id), _keywordAnalyzer } });
PerFieldAnalyzerWrapper در حقیقت برای تمام فیلدهایی که در قسمت دیکشنری فوق، ذکر نشدهاند، از LowerCaseHtmlStripAnalyzer استفاده میکند. برای مابقی موارد از KeywordAnalyzer کمک خواهد گرفت.
4) روش صحیح راه اندازی reader و writer های ایندکس لوسین جدید
کار با لوسین به حدی سریع است که از کیفیت آن شگفت زده خواهید شد! اما ... بهشرطی که بدانید دقیقا به چه صورتی باید نویسنده و خوانندهی ایندکسهای آنرا مدیریت کنید. اکثر مثالهایی را که بر روی اینترنت پیدا میکنید، به همراه متدهایی هستند که مدام در حال گشودن و dispose این نویسندهها و خوانندههای ایندکس هستند که ... این مثالها، روش کار صحیح با لوسین نیستند! و به شدت آنرا کند میکنند.
نکتهی مهمی که این مثالها به آن توجهی نکردهاند، «thread-safe» بودن نویسنده و خوانندهی ایندکس لوسین است. یعنی میتوان یک نمونه از اینها را در ابتدای کار برنامه ایجاد کرد و تا آخر کار برنامه، بدون نیاز به نمونه سازی مجدد و باز و بسته کردن آنها، بارها مورد استفادهی مجدد قرار داد و هیچ تداخلی هم ندارند و از قسمتهای مختلف برنامه هم قابل دسترسی هستند.
به همین جهت باید یک سرویس مرکزی را برای اینکار تدارک دید که طول عمر آن، حتما Singleton باشد تا بتواند نویسنده و خوانندهی ایندکس لوسین را فقط یکبار نمونه سازی و ایجاد کرده و تا پایان کار برنامه، زنده نگه دارد (کدهای کامل این کلاس را در اینجا میتوانید مطالعه کنید):
public class FullTextSearchService : IFullTextSearchService { private const LuceneVersion LuceneVersion = Lucene.Net.Util.LuceneVersion.LUCENE_48; private readonly Analyzer _analyzer; private readonly IAppFoldersService _appFoldersService; private readonly FSDirectory _fsDirectory; // IndexWriter instances are completely thread safe, meaning multiple threads can call any of its methods, concurrently. private readonly IndexWriter _indexWriter; private readonly KeywordAnalyzer _keywordAnalyzer; private readonly ILogger<FullTextSearchService> _logger; private readonly LowerCaseHtmlStripAnalyzer _lowerCaseHtmlStripAnalyzer; // Safely shares IndexSearcher instances across multiple threads, while periodically reopening. private readonly SearcherManager _searcherManager; private bool _isDisposed; public FullTextSearchService(IAppFoldersService appFoldersService, ILogger<FullTextSearchService> logger) { _appFoldersService = appFoldersService ?? throw new ArgumentNullException(nameof(appFoldersService)); _logger = logger; _keywordAnalyzer = new KeywordAnalyzer(); _lowerCaseHtmlStripAnalyzer = new LowerCaseHtmlStripAnalyzer(LuceneVersion); _analyzer = new PerFieldAnalyzerWrapper(_lowerCaseHtmlStripAnalyzer, new Dictionary<string, Analyzer> { // Document StringField instances are sort of keywords, they are not analyzed, they indexed as is (in its original case). // But StandardAnalyzer applies lower case filter to a query. // We can fix this by using KeywordAnalyzer with our query parser. { nameof(WhatsNewItemModel.Id), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentTypeIdHash), _keywordAnalyzer }, { nameof(WhatsNewItemModel.DocumentContentHash), _keywordAnalyzer } }); _fsDirectory = FSDirectory.Open(_appFoldersService.LuceneIndexFolderPath); _indexWriter = new IndexWriter(_fsDirectory, new IndexWriterConfig(LuceneVersion, _analyzer)); _searcherManager = new SearcherManager(_indexWriter, applyAllDeletes: true, searcherFactory: null); }
این سرویس، یک سرویس Singleton است که نحوهی آغاز و شروع به کار با اشیاء لوسین را در سازندهی آن مشاهده میکنید.
توضیحات:
الف) در اینجا، روش نمونه سازی PerFieldAnalyzerWrapper را که پیشتر در مورد آن بحث شد، مشاهده میکنید.
ب) سپس یک IndexWriter، نمونه سازی میشود که از تحلیلگر ترکیبی ما استفاده میکند.
ج) در ادامه یک SearcherManager جدید را مشاهده میکنید که با IndexWriter برنامه هماهنگ است و هر زمانیکه سندی به لوسین اضافه میشود، قادر به کوئری گرفتن از آن هم خواهیم بود.
نکتهی مهم: طول عمر تمام این موارد، با طول عمر کلاس سرویس جاری، یکی است. یعنی تنها یکبار در طول عمر برنامه نمونه سازی شده و تا پایان کار آن، زنده نگه داشته میشوند.
5) روش افزودن یک سند به ایندکس لوسین و سپس به روز رسانی آن
اکنون با استفاده از نگاشتگرهایی که در ابتدای بحث تهیه کردیم و همچنین شیء IndexWriter فوق، به صورت زیر میتوان یک شیء سفارشی خود را به ایندکس لوسین اضافه کنیم:
_indexWriter.AddDocument(post.MapToLuceneDocument()); _indexWriter.Flush(triggerMerge: true, applyAllDeletes: true); _indexWriter.Commit();
و یا اگر خواستیم سند موجودی را به روز کنیم، روش کار به شکل زیر است:
_indexWriter.UpdateDocument(new Term(nameof(WhatsNewItemModel.Id), post.Id.ToString()), post.MapToLuceneDocument());
new Term، در حقیقت یک کوئری جدید را سبب میشود که توسط آن سندی یافت شده، در پشت صحنه حذف میشود و سپس سند جدیدی بجای آن درج خواهد شد. در اینجا باید دقت داشت که چون Id ثبت شده از نوع StringField است، نباید حالت lower-case آنرا جستجو کرد و باید دقیقا به همان نحوی که ثبت شده، جستجو شود.
6) روش کار با searcherManager جدید لوسین
همانطور که عنوان شد، لوسین جدید به همراه یک searcherManager هم هست که کار آن، ارائهی thread-safe دسترسی به خوانندهی ایندکس لوسین است. نحوهی عمومی کار با آن را در ادامه مشاهده میکنید:
private TResult DoSearch<TResult>(Func<IndexSearcher, TResult> action, TResult defaultValue) { _searcherManager.MaybeRefreshBlocking(); var indexSearcher = _searcherManager.Acquire(); try { return action(indexSearcher); } catch (FileNotFoundException) { // It's not indexed yet. return defaultValue; } finally { _searcherManager.Release(indexSearcher); } }
با استفاده از searcherManager، در طول مدت زمان کوتاهی، بر روی ایندکس قفلگذاری شده و یک indexSearcher امن، در اختیار متدهای استفاده کنندهی از آن قرار میگیرند و در پایان کار، این قفل رها میشود.
برای مثال یک نمونه روش استفاده از این indexSearcher امن، به صورت زیر است:
public int GetNumberOfDocuments() => DoSearch(indexSearcher => indexSearcher.IndexReader.NumDocs, defaultValue: 0);
مابقی مثالهای آنرا میتوانید در کلاس FullTextSearchService مشاهده کنید که به همراه یافتن «مطالب مشابه»، جستجوهای صفحه بندی شده، جستجوهای مرتب شدهی بر اساس یک فیلد، امکان دسترسی به تمام اسناد ذخیره شدهی در ایندکس لوسین و امثال آن است که کلیات آن با قبل تفاوتی نکردهاست و مطالب و نکات آنرا پیشتر در مقالات سری لوسین بررسی کردهایم. تنها تفاوت مهمی که در اینجا وجود دارد، نحوهی برپایی و راه اندازی تحلیلگر، خواننده و نویسندهی ایندکس آن است که در این مطلب بررسی شدند؛ وگرنه کلیات جستجوی پیشرفتهی آن، مانند قبل است و تفاوت خاصی نکردهاست.
به صورت خلاصه:
- اگر فقط از کامپوننتهای کلاسی استفاده میکنید، mobx-react@5 برای کار شما پاسخگو است.
- اگر از کامپوننتهای کلاسی و همچنین کامپوننتهای تابعی در برنامهی خود استفاده میکنید، mobx-react@6 به همراه mobx-react-lite نیز ارائه میشود و هر دو روش را با هم پوشش میدهد.
- اگر فقط از کامپوننتهای تابعی جدید استفاده میکنید، هوکهای کتابخانهی کوچک mobx-react-lite برای کار شما کافی است.
معرفی useLocalStore Hook و useObserver Hook
در مطالب قبلی، روش تعریف یک کلاس مخزن حالت MobX را توسط تزئین کنندههایی مانند observable، computed و action بررسی کردیم. همچنین دریافتیم که تعریف یک چنین تزئین کنندههایی، یا نیاز به استفادهی از تایپاسکریپت را دارد و یا باید پروژهی React را جهت تغییر کامپایلر Babel آن و فعالسازی decorators، مقداری ویرایش کرد. با useLocalStore Hook هرچند تمام روشهای قبلی هنوز هم پشتیبانی میشوند، اما دیگر نیاز به استفادهی از decorators نیست. useLocalStore تابعی است که یک شیء را باز میگرداند. هر خاصیتی از این شیء، به صورت خودکار observable درنظر گرفته میشود. تمام getters آن به عنوان computed properties تفسیر میشوند و تمام متدهای آن، action درنظر گرفته خواهند شد.
یک مثال:
import React from 'react' import { useLocalStore, useObserver } from 'mobx-react' // 6.x export const SmartTodo = () => { const todo = useLocalStore(() => ({ title: 'Click to toggle', done: false, toggle() { todo.done = !todo.done }, get emoji() { return todo.done ? '😜' : '🏃' }, })) return useObserver(() => ( <h3 onClick={todo.toggle}> {todo.title} {todo.emoji} </h3> )) }
- روش استفادهی از تابع useLocalStore، میتواند به صورت محلی (همانند اسم آن) مختص به یک کامپوننت باشد. یعنی میتوان بجای state استاندارد React که اجازهی تغییر مستقیم خواص آنرا نمیدهد، از MobX State محلی ارائه شدهی توسط useLocalStore استفاده کرد و یا میتوان useLocalStore را به صورت global نیز تعریف کرد که در ادامهی بحث به آن میپردازیم.
- در مثال فوق، طول عمر شیء ایجاد شدهی توسط useLocalStore، محلی و محدود به طول عمر کامپوننت تابعی تعریف شدهاست.
- در اینجا شیء بازگشت داده شدهی توسط useLocalStore، دارای دو خاصیت title و done است. این دو خاصیت بدون نیاز به هیچ تعریف خاصی، observable در نظر گرفته میشوند. Fi به علاوه خاصیت getter آن به نام emoji نیز به عنوان یک خاصیت محاسباتی MobX تفسیر شده و متد toggle آن به صورت یک action پردازش میشود. بنابراین در حین کار با MobX Hooks دیگر نیازی به تغییر ساختار پروژهی React، برای پشتیبانی از decorators نیست.
- در این مثال، return useObserver را نیز مشاهده میکنید. کار آن رندر مجدد کامپوننت، با تغییر یکی از خواص observable ردیابی شدهی توسط آن است.
امکان تعریف global state با کمک useLocalStore
نام useLocalStore از این جهت انتخاب شدهاست که مشخص کند مخزن حالت ایجاد شدهی توسط آن، درون یک کامپوننت به صورت محلی ایجاد میشود و سراسری نیست. اما این نکته به این معنا نیست که نمیتوان مخزن حالت ایجاد شدهی توسط آنرا در بین سلسه مراتب کامپوننتهای برنامه به اشتراک گذاشت. توسط تابع useLocalStore میتوان چندین مخزن حالت را ایجاد کرد و سپس توسط شیءای دیگر آنها را یکی کرده و در آخر به کمک Context API خود React آنرا در اختیار تمام کامپوننتهای برنامه قرار داد.
تا نگارش MobX 5x (و همچنین پس از آن)، توسط inject@ میتوان یک مخزن حالت را در اختیار یک کامپوننت قرار داد (مانند inject('myStore')). طراحی inject@ مربوط است به زمانیکه امکان دسترسی به Context پشت صحنهی React به صورت عمومی توسط Context API آن ارائه نشده بود. به همین جهت از این پس دیگر نیازی به استفادهی از آن نیست.
چگونه توسط MobX Hooks، یک مخزن حالت سراسری را ایجاد کنیم؟
برای ایجاد یک مخزن حالت سراسری با روش جدید MobX Hooks، مراحل زیر را میتوان طی کرد:
الف) ایجاد شیء store
ابتدا متدی را مانند createStore ایجاد میکنیم، به نحوی که یک شیء را بازگشت دهد. این شیء همانطور که عنوان شد، خواصش، getters و متدهای آن، توسط MobX ردیابی خواهند شد (مانند const todo = useLocalStore مثال فوق) و نیازی به اعمال MobX Decorators را ندارند.
export function createStore() { return { // ... } }
ب) برپایی Context
اینبار دیگر نه از شیء Provider خود MobX استفاده میکنیم و نه از تزئین کنندهی inject@ آن؛ بلکه از React Context استاندارد استفاده خواهیم کرد:
import React from 'react'; import { createStore } from './createStore'; import { useLocalStore } from 'mobx-react'; // 6.x or mobx-react-lite@1.4.0 const storeContext = React.createContext(null); export const StoreProvider = ({ children }) => { const store = useLocalStore(createStore); return <storeContext.Provider value={store}>{children}</storeContext.Provider>; } export const useStore = () => { const store = React.useContext(storeContext); if (!store) { throw new Error('useStore must be used within a StoreProvider.'); } return store }
- سپس توسط React.createContext، یک شیء Context استاندارد React را ایجاد میکنیم؛ به نام storeContext.
- تابع کمکی StoreProvider، جایگزین شیء Provider قبلی MobX میشود. یعنی کارش محصور کردن کامپوننت App برنامه است تا شیء store را در اختیار سلسه مراتب کامپوننتهای React قرار دهد. در اینجا children به همان کامپوننتهایی که قرار است توسط Context.Provider محصور شوند اشاره میکند.
- تابع کمکی useStore، جهت محصور کردن متد React.useContext، اضافه شدهاست. میتوانید useContext Hook را به صورت مستقیم در کامپوننتهای تابعی فراخوانی کنید و یا میتوانید از متد کمکی useStore بجای آن استفاده نمائید تا حجم کدهای تکراری برنامه کاهش یابد.
ج) استفادهی از StoreProvider تهیه شده
اکنون با استفاده از متد StoreProvider فوق که شیء Context.Provider استاندارد React را بازگشت میدهد، میتوان کامپوننتهای بالاترین کامپوننت سلسه مراتب کامپوننتهای برنامه را محصور کرد، تا تمام آنها بتوانند به store ذخیره شدهی در Provider، دسترسی پیدا کنند:
export default function App() { return ( <StoreProvider> <main> <Component1 /> <Component2 /> <Component3 /> </main> </StoreProvider> ); }
د) استفاده از store مهیا شده در کامپوننتهای تابعی برنامه
پس از تهیهی متدی کمکی useStore که در حقیقت همان useContext Hook است، میتوان به کمک آن در کامپوننتهای تابعی، به store و تمام امکانات آن دسترسی پیدا کرد:
const store = useStore();
سؤال: آیا هنوز هم میتوان یک مخزن پیچیدهی متشکل از چندین کلاس را تشکیل داد؟
پاسخ: بله. برای مثال ابتدا دو کلاس جدید CounterStore و ThemeStore را به نحو متداولی، با استفادهی از MobX decorators طراحی میکنیم (دقیقا مانند مثال قسمت قبل). سپس بجای ذکر نال، بجای پارامتر متد createContext، آنرا با یک شیء جدید مقدار دهی میکنیم که هر کدام از خواص آن، به یک وهله از مخازن حالت ایجاد شده اشاره میکند:
export const storesContext = React.createContext({ counterStore: new CounterStore(), themeStore: new ThemeStore(), }); export const useStores = () => React.useContext(storesContext);
const { counterStore } = useStores();
چند نکتهی تکمیلی
نکته 1: با اشیاء MobX از Object Destructuring استفاده نکنید!
اگر بر روی اشیاء MobX از Object Destructuring استفاده کنیم، خروجی آن تبدیل به متغیرهای سادهای خواهند شد که دیگر ردیابی نمیشوند.
برای مثال اگر counterStore مثال فوق به همراه خاصیت observable ای به نام activeUserName است، آنرا به صورت زیر تبدیل به متغیر activeUserName نکنید؛ چون دیگر reactive نخواهد بود:
const { counterStore: { activeUserName }, } = useStores();
const { counterStore } = useStores();
نکته 2: مدیریت side effects با MobX
در مورد اثرات جانبی و side effects در مطلب «قسمت 32 - React Hooks - بخش 3 - نکات ویژهی برقراری ارتباط با سرور» بیشتر بحث شد. اگر یک اثر جانبی مانند تنظیم document.title، به مقدار یک خاصیت observable وابسته بود، میتوان از متد autorun که تغییرات آنها را ردیابی میکند، درون useEffect Hook استاندارد، استفاده کرد:
import React from 'react' import { autorun } from 'mobx' function useDocumentTitle(store) { React.useEffect( () => autorun(() => { document.title = `${store.title} - ${store.sectionName}` }), [], // note empty dependencies ) }
نکته 4: روش فعالسازی MobX strict mode
اگر strict mode را در Mobx به روش زیر فعال کنیم:
import { configure } from "mobx"; configure({ enforceActions: true });
نکته 3: روش انجام اعمال async در MobX
فرض کنید یک عملیات async را در یک اکشن متد کلاس حالت MobX، به صورت زیر انجام دادهایم و نتیجهی آن به خاصیت weatherData آن کلاس که observable است، به صورت مستقیم انتساب داده شدهاست:
@action loadWeather = city => { fetch( `https://abnormal-weather-api.herokuapp.com/cities/search?city=${city}` ) .then(response => response.json()) .then(data => { this.weatherData = data; }); };
راه حل اول: تغییر خاصیت this.weatherData را به یک اکشن متد مجزا انتقال میدهیم:
@action setWeather = data => { this.weatherData = data; };
راه حل دوم: اگر نمیخواهیم یک اکشن متد جدید را تعریف کنیم، میتوان از متد کمکی runInAction در داخل یک callback استفاده کرد:
loadWeatherInline = city => { fetch(`http://jsonplaceholder.typicode.com/comments/${city}`) .then(response => response.json()) .then(data => { runInAction(() => (this.weatherData = data)); }); };
در مورد اعمال async/await چطور؟
در اینجا هم تفاوتی نمیکند. هر چیزی پس از await، شبیه به حالت متد then پردازش میشود. به همین جهت در اینجا نیز باید از یکی از دو راه حل ارائه شده، استفاده کرد:
loadWeatherAsync = async city => { const response = await fetch( `http://jsonplaceholder.typicode.com/comments/${city}` ); const data = await response.json(); runInAction(() => { this.weatherData = data; }); };
- امکان عدم ذکر بعضی از پارامترهای Lambdas
- امکان تعریف متدهای static anonymous
امکان عدم ذکر بعضی از پارامترهای Lambdas در C# 9.0
مثال زیر را در نظر بگیرید:
Action<object, EventArgs> handler1 = (object obj, EventArgs args) => ShowDialog();
Action<object, EventArgs> handler2 = (object _, EventArgs _) => ShowDialog(); // OR Action<object, EventArgs> handler3 = (_, _) => ShowDialog();
نمونهی دیگر آن در حین تعریف رخدادگردانها است:
button1.Click += (s, e) => ShowDialog();
button1.Click += (_, _) => ShowDialog();
امکان تعریف Static anonymous functions در C# 9.0
برای کاهش میزان تخصیص حافظهی در حین کار با عبارات lambda ای که از متغیرهای محلی استفاده میکنند، اینبار در C# 9.0 میتوان این عبارات را static تعریف کرد. برای نمونه مثال زیر را درنظر بگیرید:
namespace CS9Features { public class LambdasTests { public void Test() { string text = "{0} is a good user !"; PrintItems(item => string.Format(text, item)); } private void PrintItems(Func<string, string> formatFunc) { foreach (var item in new[] { "User 1", "User 2" }) { Console.WriteLine(formatFunc(item)); } } } }
const string text = "{0} is a good user !"; PrintItems(static item => string.Format(text, item));
- این متد به عنوان static anonymous function شناخته میشود.
- دیگر نمیتواند دسترسی به حالت scope جاری را داشته باشد. بنابراین دیگر دسترسی به متغیرها، پارامترها و حتی شیء this را هم نخواهد داشت.
- میتواند با سایر اعضای استاتیک scope جاری کار کند.
- میتواند به تعاریف const مربوط به scope جاری، دسترسی داشته باشد.