public class Idea { public static List<Idea> Ideas = new List<Idea> { new Idea{Content="سایتی که در ایده به اشتراک گذاشته شود",Title = "سایت ایده ها"}, new Idea{Content="عینک گوگل را فارسی کنم",Title = "عینک گوگل"}, }; public string Content { get; set;} public string Title { get; set; } }
[TestFixture] public class IdeaTest { [Test] public void ShouldDisplayListOfIdea() { var viewResult = new IdeaController().Index() as ViewResult; Assert.AreEqual(Idea.Ideas, viewResult.Model) Assert.IsNotNull(viewResult.Model); } }
public class IdeaController : Controller { public ActionResult Index() { return View(Idea.Ideas); } }
[Test] public void ShouldLoadCreateIdeaView() { var viewResult = new IdeaController().Create() as ViewResult; Assert.AreEqual(string.Empty, viewResult.ViewName); }
حال نوبت به پیاده سازی اکشن رسید.:
public ActionResult Create() { return View(); }
[Test] public void ShouldAddIdeaItem() { var idea = new Idea { Title = "شبکه اجتماعی ", Content = " شبکه اجتماعی سینمایی" }; var redirectToRouteResult = new IdeaController().Create(idea) as RedirectToRouteResult; Assert.Contains(idea, Idea.Ideas); Assert.AreEqual("Index",redirectToRouteResult.RouteValues["action"]); }
public ActionResult Create(Idea idea) { Idea.Ideas.Add(idea); return RedirectToAction("Index"); }
دستیابی به HttpContext در Blazor Server
NOSQL قسمت اول
با فراگیر شدن اینترنت در سالهای اخیر و افزایش کاربران ، سیستمهای RDBMS جوابگوی نیازهای برنامهنویسان در حوزهی وب نبودند زیرا نیاز به نگهداری دادهها با حجم بالا و سرعت خواندن و نوشتن بالا از جمله نقط ضعف سیستمهای RDBMS میباشد ، چرا که با افزایش شدید کاربران دادهها اصولا به صورت منطقی ساختار یکدست خود را جهت نگهداری از دست میدهند و به این ترتیب عملیات نرمال سازی منجر به ساخت جداول زیادی میشود که نتیجه آن برای هر کوئری عملیات Joinهای متعدد میباشد که سرعت خواندن و نوشتن را به خصوص برای برنامههای با گسترهی وب پایین میآورد و مشکلات دیگری در سیستمهای RDBMS که ویژگیهای سیستمهای NoSql مشخص کننده آن مشکلات است که در ادامه به آن میپردازیم.
طبق تعریف کلی پایگاه داده NOSql عبارت است از:
نسل بعدی پایگاه داده (نسل از بعد RDBMS ) که اصولا دارای چند ویژگی زیر باشد:
۱- دادهها در این سیستم به صورت رابطهای (جدولی) نمیباشند
۲-دادهها به صورت توزیع شده نگهداری میشوند.
۳-سیستم نرمافزاری متن باز میباشد.
۴-پایگاه داده مقیاس پذیر به صورت افقی میباشد(در مطالب بعدی توضیح داده خواهد شد.)
همانگونه که گفته شد این نوع پایگاه داده به منظور رفع نیازهای برنامههای با حجم ورود و خروج داده بسیار بالا (برنامههای مدرن وب فعلی) ایجاد شدند.
شروع کار پیادهسازی این سیستمها در اوایل سال ۲۰۰۹ شکل گرفت و با سرعت زیادی رشد کرد و همچنین ویژگیهای کلی دیگری نیز به این نوع سیستم اضافه شد.
که این ویژگیها عبارتند از:
- Schema-free
: بدون شَما ! ، با توجه به برنامههای وبی فعلی ممکن است شمای نگهداری
دادهها ( ساختار کلی ) مرتبا و یا گهگاهی تغییر کند. لذا در این سیستمها
اصولا دادهها بدون شمای اولیه طراحی و ذخیره میشوند. ( به عنوان مثال
میتوان در یک سیستم که مشخصات کاربران وارد سیستم میشود برای یک کاربر یک
سری اطلاعات اضافی و برای کاربری دیگر از ورود اطلاعات اضافی صرفنظر کرد ،
و در مقایسه با RDBMS به این ترتیب از ورود مقادیر Null و یا پیوندهای
بیمورد جلوگیری کرد.
کنترل اطلاعات الزامی توسط لایه سرویس برنامه انجام میشود. ( در زبان جاوا توسط jsr-303 و یاBean Validation ها) - easy replication support : در این سیستم ، نحوهی گرفتن نسخههای پشتیبان و sync بودن نسخههای مختلف بسیار ساده و سر راست میباشد و سرور پایگاه داده به محض عدم توانایی خواندن و یا نوشتن از روی دیسک سراغ نسخهی پشتیبان میرود و آن نسخه را به عنوان نسخهی اصلی در نظر میگیرد.
- Simple API : به دلیل متنباز بودن و فعال بودن Community این سیستمها APIهای ساده و بهینهای برای اکثر زبانهای برنامهنویس محبوب ایجاد شده است که در پستهای بعدی با ارائه مثال آنها را بررسی خواهیم کرد.
- eventually consistent : در سیستمهای RDBMS که دادهها خاصیت ACID را ( در قالب Transaction) پیاده میکنند ، در این سیستمهای دادهها در وضعیت BASE قرار دارند که سرنام کلمات Basically Available ، Soft State ، Eventual Consistency میباشد.
- huge amount of data: این سیستمها به منظور کار با دادههای با حجم بالا ایجاد شدهاند ، یک تعریف کلی میگوید اگر مقدار دادههای نگهداری شده در پایگاههای داده برنامه شما ظرفیتی کمتر از یک ترابایت داده دارد از پایگاه داده RDBMS استفاده کنید واگر ظرفیت آن از واحد ترابایت فراتر میرود از سیستمهای NOSql استفاده کنید.
جهت پیاده سازی پایگاه داده با این سیستمها تا حدودی نگرش کلی به دادهها و نحوهی چیدمان آنها تغییر میکند ، به صورت کلی مباحث مربوط به normalization و de-normalization و تصور دادهها به صورت جدولی کنار میرود.
سیستم NoSql به جهت دستهبندی نحوهی ذخیرهسازی دادهها و ارتباط بین آنها به ۴ دسته کلی تقسیم میشود که معرفی کلی آن دستهبندیها موضوع مطلب بعدی میباشد.
UserDialogs.Init(this); // Before Forms.Init
containerBuilder.RegisterInstance(UserDialogs.Instance);
public IUserDialogs UserDialogs { get; set; } public async Task Login() { if (string.IsNullOrWhiteSpace(UserName) || string.IsNullOrWhiteSpace(Password)) await UserDialogs.AlertAsync(message: "Please provide UserName and Password!", title: ")-:", okText: "Ok!"); }
using (UserDialogs.Loading("Logging in...", maskType: MaskType.Black)) { // Login implementation ... await Task.Delay(TimeSpan.FromSeconds(3)); }
await NavigationService.NavigateAsync("/Login", animated: false);
await NavigationService.NavigateAsync("/Nav/HelloWorld");
3- ممکن است بخواهید هنگام رفتن از صفحهای به صفحه دیگر، پارامتر نیز ارسال کنید. اگر برای مثال صفحه اول لیست محصولات را نمایش میدهد و با زدن روی هر محصول قرار است به صفحهای برویم که جزئیات آن محصول را ببینیم، بهتر است Id آن محصول به صورت پارامتر به صفحه دوم ارسال شود. برای این کار داریم:
await NavigationService.NavigateAsync("ProductDetail", new NavigationParameters { { "productId", productId } });
حال سؤال این است که در صفحه جزئیات یک محصول، چگونه productId را بگیریم؟ فرض کنید دو صفحه ProductsList و ProductDetail را داریم. هر صفحه دارای View و View Model است. در ViewModel مربوط به ProductDetail، یعنی ProductDetailViewModel که از BitViewModelBase ارث بری کردهاست، میتوانیم متد OnNavigatedToAsync را override کنیم. در آنجا به پارامترهای ارسال شده دسترسی داریم:
public async override Task OnNavigatedToAsync(INavigationParameters parameters) { await base.OnNavigatedToAsync(parameters); Guid productId = parameters.GetValue<Guid>("productId"); }
هر ViewModel علاوه بر OnNavigatedTo می تواند دارای OnNavigatedFrom هم باشد که زمانیکه داریم از صفحه مربوطه خارج میشویم، فراخوانی میشود.
4- برای نمایش صفحه به صورت Popup کافی است بجای اینکه View ما یک Content Page باشد، یک PopupPage باشد (برای درک بهتر، فایل IntroView.xaml را در فولدر Views باز کنید).
حتی میتوانید Animation مربوط به باز شدن پاپ آپ را هم کاملا Customize کنید. مثلا زمان باز شدن، از سمت راست صفحه وارد شود و زمان خارج شدن، Fade out شود. باز کردن Popup در Navigation Page معنی نمیدهد، پس با Nav/ در اینجا کاری نداریم. در مثال ما، بعد از لاگین میخواهیم یک صفحه Intro شامل هشدارها و راهنماییهای اولیه را در قالب Popup به کاربر نمایش دهیم. Popupها میتوانند همچون Content Pageها، دارای View Model باشند و مواردی چون OnNavigatedTo، ارسال پارامتر و هر آنچه که گفته شد، در مورد آنها نیز صدق میکند.
5- برای Master/Detail کافی است بجای Nav/HelloWorld/ از MasterDetail/Nav/HelloWorld/ استفاده کنید. این عمل باعث میشود HelloWorld در داخل Navigation Page و Navigation Page داخل Master Detail باز شود. از این سادهتر امکان ندارد!
برای تغییر UI مربوط به Master که از سمت چپ باز میشود، فایل XamAppMasterDetailView.xaml را تغییر دهید.
در قسمت بعدی به جزئیات Binding خواهیم پرداخت.
به آمارهای خطای حاصل از سایت جاری که دقت میکردم یک نکته آن جالب بود: «محتوای تکراری»
mydomain.com/Home/Index mydomain.com/home/index mydomain.com/Home/index mydomain.com/home/Index
راه حل
برای حل این مساله دو نکته باید درنظر گرفته شود:
الف) هدایت دائمی (Redirect permanent) صفحات قدیمی به صفحاتی جدید، با آدرس lowercase
using System.Globalization; using System.Web; using System.Web.Mvc; namespace WebToolkit { public class ForceWww : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { modifyUrlAndRedirectPermanent(filterContext); base.OnActionExecuting(filterContext); } private static void modifyUrlAndRedirectPermanent(ActionExecutingContext filterContext) { if (canIgnoreRequest(filterContext)) return; var absoluteUrl = HttpUtility.UrlDecode(filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture)); var absoluteUrlToLower = absoluteUrl.ToLowerInvariant(); absoluteUrlToLower = forceWwwAndLowercase(filterContext, absoluteUrlToLower); absoluteUrlToLower = avoidTrailingSlashes(filterContext, absoluteUrlToLower); if (!absoluteUrl.Equals(absoluteUrlToLower)) { filterContext.Result = new RedirectResult(absoluteUrlToLower, permanent: true); } } private static string avoidTrailingSlashes(ActionExecutingContext filterContext, string absoluteUrlToLower) { if (!isRootRequest(filterContext) && absoluteUrlToLower.EndsWith("/")) return absoluteUrlToLower.TrimEnd(new[] { '/' }); return absoluteUrlToLower; } private static bool isRootRequest(ActionExecutingContext filterContext) { return filterContext.RequestContext.HttpContext.Request.Url.AbsolutePath == "/"; } private static bool canIgnoreRequest(ActionExecutingContext filterContext) { return filterContext.IsChildAction || filterContext.HttpContext.Request.IsAjaxRequest() || filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.Contains("?"); } private static string forceWwwAndLowercase(ActionExecutingContext filterContext, string absoluteUrlToLower) { if (isLocalRequet(filterContext)) return absoluteUrlToLower; if (absoluteUrlToLower.Contains("www")) return absoluteUrlToLower; return absoluteUrlToLower.Replace("http://", "http://www.") .Replace("https://", "https://www."); } private static bool isLocalRequet(ActionExecutingContext filterContext) { return filterContext.RequestContext.HttpContext.Request.IsLocal; } } }
- تمام آدرسهای سایت باید www داشته باشند؛ تا آدرسهای آن یکنواخت شده و خصوصا مشکلات لاگین و نوشته شدن کوکیها به ازای آدرسهای مختلف و سر درگمی کاربران کاهش یابد.
- اگر آدرس جاری lowercase نباشد، تبدیل به نمونه lowercase شده و درخواست کننده، به آدرس جدید هدایت میشود. این مورد خصوصا جهت موتورهای جستجو برای تصحیح نتایج آنها بسیار مفید است.
- اسلش انتهای لینکها در صورت وجود حذف خواهد شد. این مورد نیز در کاهش تعداد خطاهای «محتوای تکراری» مؤثر است.
- اگر آدرسی، کوئری استرینگ داشته باشد از آن صرفنظر خواهد شد؛ زیرا ممکن است اطلاعات موجود در آن به کوچکی و بزرگی حروف حساس باشند.
ب) کاهش بار سایت توسط تولید خودکار Urlهایی که در بدو امر lowercase هستند
برای پیاده سازی این مطلب میتوان از پروژه سورس باز «LowercaseRoutesMVC» استفاده کرد. سه فایل cs دارد که میتوانید به پروژه خود اضافه کنید. پس از آن، هرجایی در پروژه خود routes.MapRoute دارید تبدیل کنید به routes.MapRouteLowercase .
به این ترتیب به صورت خودکار تمام Urlهای تولید شده توسط HTML helpers توکار ASP.NET MVC (و نه Urlهایی که دستی نوشته شدهاند)، در حین درج در صفحه به صورت lowercase ظاهر خواهند شد (صرفنظر از اینکه نامهای کنترلرها و یا اکشن متدهای تعریف شده camel case هستند یا خیر). مزیت این مساله کاهش یک مرحله Redirect است که در قسمت الف ذکر شد. در این کتابخانه کمکی نیز از آدرسهایی که دارای کوئری استرینگ باشند، صرفنظر میشود.
AngularJS #1
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { 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) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider