[RequireHttps] public class AccountController : Controller { public IActionResult Login() { return Content("Login Page"); } }
$ openssl genrsa -out key.pem 2048 $ openssl req -new -sha256 -key key.pem -out csr.csr $ openssl req -x509 -sha256 -days 365 -key key.pem -in csr.csr -out certificate.pem openssl pkcs12 -export -out localhost.pfx -inkey key.pem -in certificate.pem
$ dotnet add package Microsoft.AspNetCore.Server.Kestrel.Https
namespace testingSSL { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) => WebHost.CreateDefaultBuilder(args) .UseKestrel(options => { options.Listen(IPAddress.Any, 8080); options.Listen(IPAddress.Any, 443, listenOptions => listenOptions.UseHttps("localhost.pfx", "qwe123456")); }) .UseStartup<Startup>() .Build(); } }
البته تا اینجا، هدف بررسی ویژگی RequireHttps بود؛ طبیعتاً به SSL در حین Development نیازی نخواهید داشت. در نتیجه میتوانیم به صورت Global تمامی کنترلرها را در زمان Production به ویژگی گفته شده مزین کنیم:
private readonly IHostingEnvironment _env; public Startup(IConfiguration configuration, IHostingEnvironment env) { Configuration = configuration; _env = env; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc(); if (!_env.IsDevelopment()) services.Configure<MvcOptions>(o => o.Filters.Add(new RequireHttpsAttribute())); }
(Http Strict Transport Security (HSTS
هدایت کردن خودکار درخواست از حالت HTTP به HTTPS، توسط خیلی از سایتها انجام میشود:
البته این روش بهتر از استفاده نکردن از SSL است؛ اما در نظر داشته باشید که همیشه اولین درخواست به صورت رمزنگاری نشده ارسال خواهد شد. فرض کنید در یک محیط پابلیک از طریق WiFi به اینترنت متصل شدهایم. شخصی (هکر) که بر روی مودم کنترل دارد، طوری WiFi را پیکربندی کردهاست که به جای آدرس اصلی که در تصویر مشاهده میکنید، یک نسخه جعلی از سایت باز شود؛ به طوریکه URL همانند URL اصلی باشد. در اینحالت کاربر به جای اینکه نامکاربری و کلمهعبور را وارد سایت اصلی کند، آن را درون سایت جعلی وارد خواهد کرد. برای حل این مشکل میتوانیم وبسایتمان را طوری تنظیم کنیم که هدر Strict-Transport-Security را به هدر اولین responseی که توسط مرورگر دریافت میشود اضافه کند:
Strict-Transport-Security: max-age=31536000
بنابراین مرورگر وبسایت را درون یک لیست internal به مدت یکسال (مقدار max-age) نگهداری خواهد کرد؛ در طول این زمان به هیچ درخواست ناامنی اجازه داده نخواهد شد. به این قابلیت HSTS گفته میشود. البته ASP.NET Core به صورت توکار روشی را جهت اضافه کردن این هدر ارائه نداده است؛ اما میتوانیم خودمان یک Middleware سفارشی را به pipeline اضافه کنیم تا اینکار را برایمان انجام دهد:
namespace testingSSL.Middleware { public class HstsMiddleware { private readonly RequestDelegate _next; public HstsMiddleware(RequestDelegate next) { _next = next; } public Task Invoke(HttpContext context) { if (!context.Request.IsHttps) return _next(context); if (IsLocalhost(context)) return _next(context); context.Response.Headers.Add("Strict-Transport-Security", "max-age=31536000"); return _next(context); } private bool IsLocalhost(HttpContext context) { return string.Equals(context.Request.Host.Host, "localhost", StringComparison.OrdinalIgnoreCase); } } }
یا اینکه میتوانیم از کتابخانه NWebSec استفاده کنیم:
$ dotnet add package NWebsec.AspNetCore.Middleware
برای استفاده از آن نیز خواهیم داشت:
app.UseHsts(h => h.MaxAge(days: 365));
اما هنوز یک مشکل وجود دارد؛ هنوز مشکل اولین درخواست را برطرف نکردهایم. زیرا مرورگر برای دریافت این هدر نیاز به مراجعه به سایت دارد. برای حل این مشکل میتوانید آدرس وبسایت خود را در سایت hstspreload ثبت کنید، سپس متد PreLoad را به کد فوق اضافه کنید:
app.UseHsts(h => h.MaxAge(days: 365).Preload());
در اینحالت حتی اگر کسی به وبسایت شما مراجعه نکند، مرورگر میداند که باید از HTTPS استفاده کند. زیرا این لیست به صورت توکار درون مرورگر تعبیه شدهاست. بنابراین در اینحالت مطمئن خواهیم شد که تمامی connectionها به سایتمان امن میباشند.
دریافت کدهای مطلب جاری (+)
مزایای بارگذاری متادیتا از طریق یک فایل جاوااسکریپتی
در ابتدا فایل metadata.js را در مسیر Scripts ایجاد نمایید و ارجاعی به آن را در layaout.cshtml قرار دهید:
<script src="~/Scripts/metadata.js"></script>
کارهایی را که باید انجام بدهیم:
private static void WriteMetadata() { var metadata = new EFContextProvider<ApplicationDbContext>().Metadata();//1.1 var fileName =HttpContext.Current.Server.MapPath("~/Scripts/metadata/metadata.js"); const string prefix = "var metadata = JSON.stringify(";//1.2 const string postfix = ");"; using (var writer = new StreamWriter(fileName)) { writer.WriteLine(prefix + metadata + postfix);//1.3 } }
var serviceName = 'breeze/northwind'; // your service root here function createEntityManager() { var dataService = new breeze.DataService({ serviceName: serviceName, hasServerMetadata: false // 2.1 }); var metadataStore = new breeze.MetadataStore({ namingConvention: camelCaseConvention // if you use this convention }); // initialize it from the application's metadata variable metadataStore.importMetadata(metadata);//2.2 return new breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore });//2.3 }
دریافت Twitter Bootstrap
محل اصلی دریافت Twitter Bootstrap، آدرس ذیل است:
البته ما از آن در اینجا به شکل خام فوق استفاده نخواهیم کرد؛ زیرا نیاز است قابلیتهای استفاده در محیطهای راست به چپ فارسی نیز به آن اضافه شود. برای این منظور میتوانید از یکی از دو بسته نیوگت ذیل استفاده نمائید:
RTL Twitter Bootstrap, https://nuget.org/packages/Twitter.Bootstrap.RTL
و یا حتی از منابع سایت http://rbootstrap.ir نیز میتوان استفاده کرد.
برای نمونه دستور زیر را در کنسول پاورشل ویژوال استودیو وارد نمائید تا اسکریپتها و فایلهای CSS مورد نیاز به پروژه جاری اضافه شوند:
PM> Install-Package Twitter.BootstrapRTL
در اینجا فایلهای min، نگارشهای فشرده شده فایلهای js یا css هستند که با توجه به امکانات اضافه شده به ASP.NET MVC4، از آنها استفاده نخواهیم کرد و برای افزودن و تعریف آنها از امکانات Bundling and minification توکار فریم ورک ASP.NET MVC به نحوی که در ادامه توضیح داده خواهد شد، استفاده میکنیم.
فایلهای png اضافه شده، آیکونهای مخصوص Twitter Bootstrap هستند که اصطلاحا به آنها sprite images نیز گفته میشود. در این نوع تصاویر، تعداد زیادی آیکون در کنار هم، برای بهینه سازی تعداد بار رفت و برگشت به سرور جهت دریافت تصاویر، طراحی شده و قرار گرفتهاند.
فایلهای js این مجموعه اختیاری بوده و برای استفاده از ویجتهای Twitter Bootstrap مانند آکاردئون کاربرد دارند. این فایلها برای اجرا، نیاز به jQuery خواهند داشت.
افزودن تعاریف اولیه Twitter Bootstrap به یک پروژه ASP.NET MVC
امکانات Bundling and minification در نوع پروژههای نسبتا جامعتر ASP.NET MVC به صورت پیش فرض لحاظ شده است. اما اگر یک پروژه خالی را شروع کردهاید، نیاز است بسته نیوگت آنرا نیز نصب کنید:
PM> Install-Package Microsoft.AspNet.Web.Optimization
using System.Collections.Generic; using System.IO; using System.Web; using System.Web.Optimization; namespace Mvc4TwitterBootStrapTest.Helper { /// <summary> /// A custom bundle orderer (IBundleOrderer) that will ensure bundles are /// included in the order you register them. /// </summary> public class AsIsBundleOrderer : IBundleOrderer { public IEnumerable<FileInfo> OrderFiles(BundleContext context, IEnumerable<FileInfo> files) { return files; } } public static class BundleConfig { private static void addBundle(string virtualPath, bool isCss, params string[] files) { BundleTable.EnableOptimizations = true; var existing = BundleTable.Bundles.GetBundleFor(virtualPath); if (existing != null) return; Bundle newBundle; if (HttpContext.Current.IsDebuggingEnabled) { newBundle = new Bundle(virtualPath); } else { newBundle = isCss ? new Bundle(virtualPath, new CssMinify()) : new Bundle(virtualPath, new JsMinify()); } newBundle.Orderer = new AsIsBundleOrderer(); foreach (var file in files) newBundle.Include(file); BundleTable.Bundles.Add(newBundle); } public static IHtmlString AddScripts(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Render(virtualPath); } public static IHtmlString AddStyles(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Render(virtualPath); } public static IHtmlString AddScriptUrl(string virtualPath, params string[] files) { addBundle(virtualPath, false, files); return Scripts.Url(virtualPath); } public static IHtmlString AddStyleUrl(string virtualPath, params string[] files) { addBundle(virtualPath, true, files); return Styles.Url(virtualPath); } } }
پس از افزودن کلاسهای کمکی فوق، به فایل layout پروژه مراجعه کرده و تعاریف ذیل را به ابتدای فایل اضافه نمائید:
@using Mvc4TwitterBootStrapTest.Helper <!DOCTYPE html> <html> <head> <title>@ViewBag.Title</title> @BundleConfig.AddStyles("~/Content/css", "~/Content/bootstrap.css", "~/Content/bootstrap-responsive.css", "~/Content/Site.css" ) @BundleConfig.AddScripts("~/Scripts/js", "~/Scripts/jquery-1.9.1.min.js", "~/Scripts/jquery.validate.min.js", "~/Scripts/jquery.unobtrusive-ajax.min.js", "~/Scripts/jquery.validate.unobtrusive.min.js", "~/Scripts/bootstrap.min.js" ) @RenderSection("JavaScript", required: false) </head>
<!DOCTYPE html> <html> <head> <title>Index</title> <link href="/Content/css?v=vsUQD0OJg4AJ-RZH8jSRRCu_rjl2U1nZrmSsaUyxoAc1" rel="stylesheet"/> <script src="/Scripts/js?v=GezdoTDiWY3acc3mI2Ujm_7nKKzh6Lu1Wr8TGyyLpW41"></script> </head>
مفاهیم پایهای Twitter Bootstrap
الف) Semantic class names
به عبارتی کلاسهای Twitter Bootstrap دارای نامهایی معنا دار و مفهومی میباشند؛ مانند کلاسهای CSSایی، به نامهای Succes، Error، Info و امثال آن. این نامها مفهومی را میرسانند؛ اما در مورد نحوه پیاده سازی آنها جزئیاتی را بیان نمیکنند.
برای نمونه میتوان کلاسی را به نام redText ایجاد کرد. هر چند این نام، توضیحاتی را در مورد علت وجودیاش بیان میکند، اما بسیار ویژه بوده و در مورد جزئیات پیاده سازی آن نیز اطلاعاتی را ارائه میدهد. در این حالت redText معنایی ندارد. چرا یک Text باید قرمز باشد؟ برای مثال این متن قرمز است چون مثلا شخصی، به آن رنگ ویژه علاقه دارد، یا اینکه قرمز است بخاطر نمایش خطایی در صفحه؟ به همین جهت در Twitter Bootstrap از نامهای مفهومی یاده شده، مانند Error استفاده میشود. نامهایی معنا دار اما بدون دقیق شدن در مورد ریز جزئیات پیاده سازی آنها. در این حالت میتوان قالب جدیدی را تدارک دید و با ارائه تعاریف جدیدی برای کلاس Error و نحوه نمایش دلخواهی را به آن اعمال نمود.
یا برای نمونه نام rightside را برای نمایش ستونی در صفحه، درنظر بگیرید. این نام بسیار ویژه است؛ اما Sematic name آن میتواند sidebar باشد تا بدون دقیق شدن در جزئیات پیاده سازی آن، در چپ یا راست صفحه قابل اعمال باشد.
Semantic class names کلیدهایی هستند جهت استفاده مجدد از قابلیتهای یک فریم ورک CSS.
ب) Compositional classes
اکثر کلاسهای Twitter Bootstrap دارای محدوده کاری کوچکی هستند و به سادگی قابل ترکیب با یکدیگر جهت رسیدن به نمایی خاص میباشند. برای مثال به سادگی میتوان به یک table سه ویژگی color، hover و width برگرفته شده از Twitter Bootstrap را انتساب داد و نهایتا به نتیجه دلخواه رسید؛ بدون اینکه نگران باشیم افزودن کلاس جدیدی در اینجا بر روی سایر کلاسهای انتساب داده شده، تاثیر منفی دارد.
ج) Conventions
برای استفاده از اکثر قابلیتهای این فریم ورک CSS یک سری قراردادهای پیش فرضی وجود دارند. برای مثال اگر از کلاس توکار pagination به همراه یک سری ul و li استفاده کنید، به صورت خودکار یک pager شکیل ظاهر خواهد شد. یا برای مثال اگر به یک html table کلاسهای table table-striped table-hover را انتساب دهیم، به صورت خودکار قراردادهای پیش فرض table مجموعه Twitter Bootstrap به آن اعمال شده، به همراه رنگی ساختن یک درمیان زمینه ردیفها و همچنین فعال سازی تغییر رنگ ردیفها با حرکت ماوس از روی آنها.
طرحبندی صفحات یک سایت به کمک Twitter Bootstrap
بررسی Grid layouts
Layout به معنای طرحبندی و چیدمان محتوا در یک صفحه است. یکی از متداولترین روشهای طرحبندی صفحات چه در حالت چاپی و چه در صفحات وب، چیدمان مبتنی بر جداول و گریدها است. از این جهت که نحوه سیلان و نمایش محتوا از چپ به راست و یا راست به چپ را به سادگی میسر میسازند؛ به همراه اعمال حاشیههای مناسب جهت قسمتهای متفاوت محتوای ارائه شده. Grid در طرحبندی، نمایش بصری نخواهد داشت اما در ساختار صفحه وجود داشته و مباحثی مانند جهت، موقعیت و یکپارچگی و یکدستی طراحی را سبب میشود.
به علاوه مرورگرها و مفهوم Grid نیز به خوبی با یکدیگر سازگار هستند. در دنیای HTML و ،CSS طراحیها بر اساس مفهوم ساختار مستطیلی اشیاء صورت میگیرد:
برای نمونه در اینجا تصویر CSS Box Model را مشاهده میکنید. به این ترتیب، هر المان دارای محدودهای مستطیلی با طول و عرض مشخص، به همراه ویژگیهایی مانند Margin، Border و Padding است.
در سالهای اولیه طراحی وب، عموما کارهای طراحی صفحات به کمک HTML Tables انجام میشد. اما با پختهتر شدن CSS، استفاده از Tables برای طراحی صفحات کمتر و کمتر گشت تا اینکه نهایتا فریم ورکهای CSS ایی پدید آمدند تا طراحیهای مبتنی بر CSS را با ارائه گریدها، سادهتر کنند. مانند Blue print، 960 GS و ... Twitter Bootstrap که طراحی مبتنی بر گریدهای CSS ایی را به مجموعه قابلیتهای دیگر خود افزوده است.
بررسی Fixed Grids
در اینجا در صفحه layout برنامه، یک Div دربرگیرنده دو Div دیگر را مشاهده میکنید:
<body> <div> <div> <h1> Title Title </h1> Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text </div> <div> @RenderBody() </div> </div> </body>
برای اینکار در Twitter Bootstrap از کلاسی به نام row استفاده میشود که بیانگر یک ردیف است. این کلاس را به خارجیترین Div موجود اعمال خواهیم کرد. در یک صفحه، هر تعداد row ایی را که نیاز باشد، میتوان تعریف کرد. داخل این ردیفها، امکان تعریف ستونهای مختلف و حتی تعریف ردیفهای تو در تو نیز وجود دارد. هر ردیف Twitter Bootstrap از 12 ستون تشکیل میشود و برای تعریف آنها از کلاس span استفاده میگردد. در این حالت جمع اعداد ذکر شده پس از span باید 12 را تشکیل دهند.
<body> <div class="row"> <div class="span7"> <h1> Title Title </h1> Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text Text </div> <div class="span5"> @RenderBody() </div> </div> </body>
در این تصویر، قسمت RenderBody کار رندر اکشن متد Index کنترلر Home برنامه را با Viewایی معادل کدهای ذیل، انجام داده است:
@{ ViewBag.Title = "Index"; } <h2> Index</h2> <div class="hero-unit"> <h2>@ViewBag.Message</h2> <p> This is a template to demonstrate the way to beautify the default MVC template using Twitter Bootstrap website. It is pretty simple and easy.</p> <p> <a href="http://asp.net/mvc" class="btn btn-primary btn-large" style="color: White;"> To learn more about ASP.NET MVC visit »</a></p> </div>
درک نحوه عملکرد Grid در Twitter Bootstrap
در مثال ذیل 5 ردیف را مشاهده میکنید:
<div class="row"> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> <div class="span1">1</div> </div> <div class="row"> <div class="span3">3</div> <div class="span4">4</div> <div class="span5">5</div> </div> <div class="row"> <div class="span5">5</div> <div class="span7">7</div> </div> <div class="row"> <div class="span3">3</div> <div class="span7 offset2">7 offset 2</div> </div> <div class="row"> <div class="span12">12</div> </div>
در ستون چهارم، از کلاس offset نیز استفاده شده است. این مورد سبب میشود ستون جاری به تعدادی که مشخص شده است به سمت چپ (با توجه به استفاده از حالت RTL در اینجا) رانده شود و سپس ترسیم گردد.
یا اینکه میتوان مانند ردیف آخر، یک ستون را به عرض 12 که در حقیقت 940 پیکسل است، ترسیم نمود.
برای اینکه بتوانیم این گرید تشکیل شده و همچنین ستونها را بهتر مشاهده کنیم، به فایل style.css سایت، تنظیم زیر را اضافه کنید:
[class*="span"] { background-color: lightblue; text-align: center; margin-top: 15px; }
نکته جالب این گرید، Responsive یا واکنشگرا بودن آن است. در این حالت، عرض مرورگر را کم و زیاد کنید. خواهید دید که ستونها در صورتیکه در عرض نمایشی جاری، قابل ارائه نباشند، به ردیفهای بعدی منتقل خواهند شد.
البته باید دقت داشت که این گرید هیچگاه یک ستون را نخواهد شکست. برای نمونه ردیف آخر، همواره با همان عرض ثابتش نمایش داده میشود و با کوچکتر کردن اندازه مرورگر، یک اسکرول افقی برای نمایش محتوای آن ظاهر خواهد شد.
یک نکته
اگر نمیخواهید که چنین رفتار واکنشگرایی بروز کند، نیاز است کلیه ردیفها را در div ایی با کلاسی به نام container محصور کنید.
به این ترتیب ابتدا گرید نمایش داده شده، در میانه صفحه ظاهر خواهد شد (پیشتر از سمت راست شروع شده بود). همچنین دیگر با کوچک و بزرگ شدن اندازه مرورگر، ستونها به شکل یک پشته بر روی هم قرار نخواهند گرفت. (اگر پس از این تنظیم، چنین قابلیتی را مشاهده نکردید و هنوز هم طراحی، واکنشگرا بود، تعریف bootstrap-responsive.css را نیاز است برای آزمایش، از هدر صفحه حذف کنید)
بررسی Fluid Grids
به گرید قسمت قبل از این جهت Fixed Grid گفته میشود که عرض هر span آن با یک عدد مشخص تعیین گشته است. اما در حالت Fluid Grid، عرض هر span برحسب درصد تعیین میشود. بکارگیری درصد در اینجا به معنای امکان تغییر عرض یک ستون بر اساس عرض جاری Container آن است. در اینجا span12 دارای عرض 100 درصد خواهد بود.
در مثال قبل، برای استفاده از Fluid grids، تنها کافی است هرجایی کلاسی مساوی row وجود دارد، به row-fluid تغییر کند. همچنین کلاس container را به container-fluid تغییر دهید.
برای آزمایش آن، اندازه و عرض نمایشی مرورگر خود را تغییر دهید. اینبار مشاهده خواهید کرد که برخلاف حالت Fixed Grid، عرض ستونها به صورت خودکار کم و زیاد میشوند. این مورد بر روی محتوای قرار گرفته در این ستونها نیز تاثیر گذار است. برای مثال اگر یک تصویر را در حالت Fluid grid در ستونی قرار دهید، با تغییر عرض مرورگر، اندازه این تصویر نیز تغییر خواهند کرد؛ اما در حالت Fixed Grid خیر.
حالت Fluid، شیوه متداول استفاده از bootstrap در اکثر سایتهای مهمی است که تابحال از این فریم ورک CSS استفاده کردهاند.
مروری بر طراحی واکنشگرا یا Responsive
این روزها تعدادی از کاربران، با استفاده از ابزارهای موبایل و تبلتها از وب سایتها بازدید میکنند. هر کدام از اینها نیز دارای اندازه نمایشی متفاوتی میباشند. بنابراین نیاز خواهد بود تا حالت بهینهای را جهت اینگونه وسایل نیز طراحی نمود. حالت بهینه در اینجا به معنای قابل خواندن بودن متون، امکان استفاده از لینکهای ورود به صفحات مختلف و همچنین حذف اسکرول و مباحث زوم جهت مشاهده صفحات است.
یکی از ویژگیهای CSS به نام media چنین قابلیتی را فراهم میسازد. برای نمونه قسمتی از فایل bootstrap-responsive.css دارای چنین تعاریفی است:
@media (min-width: 768px) and (max-width: 979px) { .hidden-desktop { display: inherit !important; }
Bootstrap برای مدیریت اندازههای مختلف وسایل موبایل، شیوهنامههای خاصی را تدارک دیده است که از اندازه px480 و یا کمتر، تا px1200 و یا بیشتر را پوشش میدهد.
به این ترتیب با اندازه px940 که پیشتر در مورد آن بحث شد، اندازه span12 به صورت خودکار به اندازههای متناسب با صفحات نمایشی کوچکتر تنظیم میگردد. همچنین برای اندازههای صفحات کوچکتر از 768px به صورت خودکار از Fluid columns استفاده میگردد.
تنها کاری که برای اعمال این قابلیت باید صورت گیرد، افزودن تعاریف فایل bootstrap-responsive.css به هدر صفحه است که در قسمت قبل انجام گردید. این فایل باید پس از فایل اصلی bootstrap.css اضافه شود.
کلاسهای کمکی طراحی واکنشگرا
Bootstrap شامل تعدادی کلاس کمکی در فایل bootstrap-responsive.css خود میباشد شامل visible-phone، visible-tablet و visible-desktop به علاوه hidden-phone، hidden-tablet و hidden-desktop. به این ترتیب میتوان محتوای خاصی را جهت وسایل ویژهای نمایان یا مخفی ساخت.
برای مثال محتوای مشخص شده با کلاس hidden-desktop، در اندازه وسایل دسکتاپ نمایش داده نخواهد شد.
برای آزمایش آن، شش div را با کلاسهای یاد شده و محتوایی دلخواه ایجاد کرده و سپس اندازه عرض مرورگر را تغییر دهید تا بهتر بتوان مخفی یا نمایان ساختن محتوا را بر اساس اندازه صفحه جاری درک کرد.
یکی از کاربردهای این قابلیت، قرار دادن تبلیغاتی با اندازههای تصاویری مشخص برای وسایل مختلف است؛ بجای اینکه منتظر شویم تا Fluid layout اندازه تصاویر را به صورت خودکار کوچک یا بزرگ کند، که الزاما بهترین کیفیت را حاصل نخواهد ساخت.
<div class="container-fluid"> <div class="row-fluid"> <div class="span4"> <div class="visible-phone"> visible-phone</div> </div> <div class="span4"> <div class="visible-tablet"> visible-tablet</div> </div> <div class="span4"> <div class="visible-desktop"> visible-desktop</div> </div> </div> </div> <div class="container-fluid"> <div class="row-fluid"> <div class="span4"> <div class="hidden-phone"> hidden-phone</div> </div> <div class="span4"> <div class="hidden-tablet"> hidden-tablet</div> </div> <div class="span4"> <div class="hidden-desktop"> hidden-desktop</div> </div> </div> </div>
معرفی پروژه NotifyPropertyWeaver
- این بسته از طریق آدرس ذیل نیز قابل دریافت است:
https://az320820.vo.msecnd.net/packages/propertychanged.fody.1.42.0.nupkg
همین آدرس را در IE وارد کنید. اگر کار نکرد احتمالا تنظیمات IE شما به هم ریخته است؛ چون تنظیمات آن به صورت مستقیم روی تنظیمات اتصالی برنامههای دات نت تاثیر دارند.
لینکهای هفته دوم دی
وبلاگها ، سایتها و مقالات ایرانی (داخل و خارج از ایران)
- شیوهنامه فارسیپسند گوگلخوان، نسخه ۰.۱.۱ (کار جالبی است. آشنایی با افزونه استایلیش)
ASP. Net
طراحی و توسعه وب
PHP
اسکیوال سرور
سی شارپ
SharePoint
عمومی دات نت
ویندوز
مسایل اجتماعی و انسانی برنامه نویسی
متفرقه
- آشنایی با لولههای یاهو و همچنین نمونهای دیگر
column.Group((val1, val2) => { return (int)val1 == (int)val2; });
- لطفا برای سؤالات بعدی از قسمت پرسش و پاسخ مرتبط با این پروژه در سایت استفاده کنید.
کاربران امروزه با عناصری که به نحوی خاص درون صفحه عمل میکنند، آشنا شدهاند. بنابراین انتخاب مناسب برای اتخاذ این عناصر زمانی مناسب است که به تکمیل کارآیی و رضایت کاربر کمک کند.
یک کامپوننت در واقع از دو بخش تشکیل شدهاست:
1 - اول اینکه چگونه به نظر میرسد ( UI ).
2 – دوم اینکه چگونه کار میکند ( UX ).
این عناصر رابط ( component ) شامل :
Input Controls : check boxes, radio buttons, drop down lists, list boxes, buttons, toggles, text fields, date field
Navigational Components : breadcrumb, slider, search field, pagination, slider, tags, icons
Informational Components : tool tips, icons, progress bar, notifications, message boxes, modal windows
Containers : accordion
اما باید توجه داشت که فقط به این موارد محدود نمیشوند.
در این
قسمت به طور مختصر با این دست از کامپوننتها ( UI Component ) آشنا میشویم.
کامپوننت V-Alert برای انتقال اطلاعات مهم به کاربر مورد استفاده قرار میگیرد. این کامپوننت چهار نوع اطلاعات را به کاربر گوشزد میکند که شامل موفقیتها، اطلاعات، هشدارها و خطاها میباشد.
هشدارها میتوانند یک رنگ خاص را داشته باشند که به طور پیش فرض نمایش داده نمیشوند.
در مثال پایین، کامپوننت v-alert شامل دو مقدار است که برای آن تنظیم شدهاست. مقدار (value) که شامل یک مقدار Boolean است و مقدار (type) که مشخص کننده نوع هشدار است (موفقیت ، اطلاعات ، هشدار و خطا).
در قطعه کد پایین، این چهار نوع اطلاعات قابل نمایش به کاربر مشخص شدهاند:
<div id="app"> <v-app id="inspire"> <div> <v-alert :value="true" type="success"> This is a success alert. </v-alert> <v-alert :value="true" type="info"> This is a info alert. </v-alert> <v-alert :value="true" type="warning"> This is a warning alert. </v-alert> <v-alert :value="true" type="error"> This is a error alert. </v-alert> </div> </v-app> </div>
برای کامپوننت V-Alert میتوان propertiesهای مختلفی را مشخص نمود که از جمله آنها میتوان به موارد زیر اشاره کرد:
Color : به وسیله این property میتوان رنگ پیغام را مشخص نمود. هم به وسیله نام رنگ میتوان رنگ مورد نظر را مشخص کرد و هم به وسیلهی کد RGB این کار را انجام داد. dismissible : این تنظیم مشخص میکند که پیغام، قابلیت بسته شدن را دارد یا خیر که حاوی یک مقدار Boolean است.
icon : مشخص کننده یک نماد خاص است که درون جعبه پیغام قرار میگیرد.
type : مشخص کننده نوع پیام است که پیشتر در مورد آن توضیح داده شد.
کامپوننت v-avatar برای تغییر اندازه تصاویر مورد استفاده قرار میگیرد که معمولا جهت نمایش عکس پروفایل استفاده میشود.
طریقه استفاده :
avatar، دارای یک اندازهی پویا است که میتواند برای هر وضعیتی تغییر کند.
برای این کامپوننت سه properties قابل تنظیم است:
color : به وسیله این property میتوان رنگ دلخواهی را برای آواتار مشخص نمود. هم به وسیله نام رنگ میتوان رنگ مورد نظر را مشخص کرد و هم به وسیله کد RGB این کار را صورت داد.
size : به طور پیشفرض برای avatar، سایز 48 تنظیم شدهاست که میتوان این میزان را کم و یا زیاد کرد.
tile : همانند border radius در css عمل میکند. با تنظیم این گزینه میتوانیم یک آواتار گرد داشته باشیم.
به وسیله این کامپوننت میتوان نمادهایی را برای نمایش اطلاعاتی به کاربر یا جلب توجه کاربر به یک عنصر خاص، ایجاد نمود.
این کامپوننت نیز properties خاص خود را دارد که از جمله آن میتوان به color , left , mode , overlab و غیره اشاره کرد.
قطعه کد پایین نشان دهنده چگونگی عملکرد این کامپوننت است:
<div id="app"> <v-app id="inspire"> <div> <v-badge color="purple" left overlap> <template v-slot:badge> <v-icon dark small> done </v-icon> </template> <v-icon color="grey lighten-1" large> account_circle </v-icon> </v-badge> <v-badge overlap color="orange"> <template v-slot:badge> <v-icon dark small> notifications </v-icon> </template> <v-icon large color="grey darken-1"> account_box </v-icon> </v-badge> </div> </v-app> </div>
نتیجه قطعه کد بالا بدین ترتیب است:
این کامپوننت را میتوان جایگزین sidebarها نمود. این کامپوننت در درجه اول در موبایل مورد استفاده قرار میگیرد که میتواند شامل متن و یا آیکن باشد.
به وسیله این کامپوننت امکان انتقال از یک بخش از صفحه به بخشی دیگر امکان پذیر میشود.
این کامپوننت نیز properties خاص خود را دارد که از جمله آن میتوان به active-sync (برای نشان دادن فعال یا غیر فعال بودن گزینه انتخاب شده)، fixed ( برای مشخص کردن موقعیت کامپوننت در صفحه) و موارد دیگر اشاره کرد.
تقسیم بندی اجزاء این کامپوننت به شرح زیر است:
1 - محل قرار گیری کامپوننت
2- آیکن غیر فعال
3- برچسب غیر فعال
4 - آیکن فعال
5- برچسب فعال
قطعه کد پایین نشان دهنده چگونگی یک bottom navbar است:
<div id="app"> <v-app id="inspire"> <v-card height="200px" flat> <div> Active: {{ bottomNav }} // </div> <v-bottom-nav :active.sync="bottomNav" :value="true" absolute color="transparent"> <v-btn color="teal" flat value="recent"> <span>Recent</span> <v-icon>history</v-icon> </v-btn> <v-btn color="teal" flat value="favorites"> <span>Favorites</span> <v-icon>favorite</v-icon> </v-btn> <v-btn color="teal" flat value="nearby"> <span>Nearby</span> <v-icon>place</v-icon> </v-btn> </v-bottom-nav> </v-card> </v-app> </div>
این کامپوننت برای ایجاد یک دکمه چه به صورت متن و یا آیکن مورد استفاده قرار میگیرد. دکمهها به کاربران این امکان را میدهند تا اقداماتی را انجام دهند و انتخابهای خود را تنها با یک کلیک انجام دهند.
از دکمهها ممکن است در جاهای مختلف صفحه به خصوص در دیالوگ باکسها، فرمها و ابزارها مورد استفاده قرار گیرد.
کامپوننت v-btn نیز مانند سایر کامپوننتها تنظیمات خاص خود را دارد که از جمله آن میتوان به کوچکی و بزرگی دکمه، فعال یا غیر فعال بودن دکمه، نوع متن یا آیکن بودن دکمه اشاره نمود.
حالتهای مختلفی از دکمهها وجود دارند که میتوانند به بهتر شدن UI برنامه ما کمک کنند. برای مثال میتوان به موارد زیر اشاره کرد:
button drop-down variants : دکمههای کرکرهای که معمولا برای نظم و کم جا بودن در صفحه مورد استفاده قرار میگیرند.
icons : آیکنها میتوانند برای محتوای اصلی یک دکمه مورد استفاده قرار بگیرند تا ظاهر زیباتری را به دکمه ما بدهند.
floating : این دکمهها حالت آیکن را دارند؛ با این تفاوت که آیکن مورد نظر، درون یک محتوا قرار میگیرد.
loaders : به وسیله این دکمهها میتوان کاربر را متوجه انجام یک پردازش نمود. به صورت پیشفرض بعد از فشردن این نوع دکمهها محتوای دکمه فشرده شده تغییر ظاهر داده و به شکل یک دایره در حال چرخش در میآید. البته میتوان این پیشفرض را به حالتهای دیگری نیز تغییر داد.
round : این نوع دکمهها دقیقا کارآیی دکمههای معمولی را دارند؛ با این تفاوت که این دکمهها دارای لبههایی گرد هستند.
یک نمونه از ایجاد انواع دکمهها در زیر آمده است:
<div id="app"> <v-app id="inspire"> <div> <v-btn color="success">Success</v-btn> <v-btn color="error">Error</v-btn> <v-btn color="warning">Warning</v-btn> <v-btn color="info">Info</v-btn> </div> </v-app> </div>
یکی از کامپوننتهایی که به تازگی به vuetify اضافه شده است، کامپوننت تقویم یا v-calendar است. از این کامپوننت برای نمایش تاریخ، روز، هفته، ماه و سال استفاده میشود. یک تقویم دارای یک نوع و یک مقدار است که تعیین میکند چه نوع تقویمی، در طول چه مدت زمانی نمایش داده شود.
حالتهای مختلفی برای نمایش تقویم در صفحه وجود دارد که برای مثال میتوان به موارد زیر اشاره کرد:
events : به وسیله این گزینه میتوان برای هر روز یک رخداد خاص را مشخص نمود که به وسیله کلیک بر روی آن، اطلاعات آن رخداد نمایش داده شود.
weekly : میتوان یک تقویم هفتگی را ایجاد نمود و رخدادهای هفتگی را برای آن تنظیم کرد.
نمونه ایجاد یک تقویم در پایین آمده است:
<div id="app"> <v-app id="inspire"> <v-layout wrap> <v-flex xs12> <v-sheet height="500"> <v-calendar ref="calendar" v-model="start" :type="type" :end="end" color="primary"> </v-calendar> </v-sheet> </v-flex> <v-flex sm4 xs12> <v-btn @click="$refs.calendar.prev()"> <v-icon dark left> keyboard_arrow_left </v-icon> Prev </v-btn> </v-flex> <v-flex sm4 xs12> <v-select v-model="type" :items="typeOptions" label="Type"> </v-select> </v-flex> <v-flex sm4 xs12> <v-btn @click="$refs.calendar.next()"> Next <v-icon right dark> keyboard_arrow_right </v-icon> </v-btn> </v-flex> </v-layout> </v-app> </div>
js قطعه کد new Vue({ el: '#app', data: () => ({ type: 'month', //مشخص کننده نوع تقویم که در اینجا تقویم به صورت ماهانه است start: '2019-01-01', end: '2019-01-06', typeOptions: [ { text: 'Day', value: 'day' }, { text: '4 Day', value: '4day' }, { text: 'Week', value: 'week' }, { text: 'Month', value: 'month' }, { text: 'Custom Daily', value: 'custom-daily' }, { text: 'Custom Weekly', value: 'custom-weekly' } ] }) })
Blazor 5x - قسمت 19 - کار با فرمها - بخش 7 - نکات ویژهی کار با EF-Core در برنامههای Blazor Server
طول عمر سرویسها، در برنامههای Blazor Server متفاوت هستند
هنگامیکه با یک ASP.NET Core Web API متداول کار میکنیم، درخواستهای HTTP رسیده، از میانافزارهای موجود رد شده و پردازش میشوند. اما هنگامیکه با Blazor Server کار میکنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشتهای برنامه به سرور و پاسخهای دریافتی، از طریق Web socket منتقل میشوند و نه درخواستها و پاسخهای متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه میگذارد، تغییر طول عمر سرویسهای آن است. برای مثال در برنامههای Web API، طول عمر درخواستها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویسهای مورد نیاز وهله سازی شده و در پایان درخواست، رها میشوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامههای Web API نیز EF-Core و DbContext آن، به صورت سرویسهایی با طول عمر Scoped تعریف میشوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
public static IServiceCollection AddDbContext<TContext>( [NotNullAttribute] this IServiceCollection serviceCollection, [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, ServiceLifetime contextLifetime = ServiceLifetime.Scoped, ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
اما زمانیکه مانند یک برنامهی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهلهای از DbContext که در اختیار برنامهی Blazor Server قرار میگیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیشفرض تعریف شده، همانند یک وهلهی با طول عمر Singleton عمل میکند. در این حالت تمام صفحات و کامپوننتهای یک برنامهی Blazor Server، از یک تک وهلهی مشخص DbContext که در ابتدای کار دریافت کردهاند، کار میکنند و از آنجائیکه DbContext به صورت thread-safe کار نمیکند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آنرا در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر میرسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمتهای مختلف برنامه، باز هم این خطا مشاهده میشود:
An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'. System.InvalidOperationException: A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا میشود؛ اما چون ابتدا و انتهای درخواستها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی میشود. به همین جهت استثنائی را که در اینجا مشاهده میکنید، در برنامههای Web API شاید هیچگاه مشاهده نشود.
معرفی DbContextFactory در EF Core 5x
همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContextها (داشتن طول عمر طولانی) در برنامههای Blazor Server هستند. یک چنین رفتاری را شاید در برنامههای دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامههای دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شدهی در آن هم برقرار است و Dispose نمیشود. در یک چنین حالتهایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها میکنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازیهایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامههای Blazor Server تحت عنوان DbContextFactory ارائه شدهاست که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامهها مورد استفاده قرار میگیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده میشود:
public void ConfigureServices(IServiceCollection services) { var connectionString = Configuration.GetConnectionString("DefaultConnection"); //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
روش اول کار با DbContextFactory در کامپوننتهای Blazor Server : وهله سازی از نو، به ازای هر متد
در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره میکند به ابتدای کامپوننت تزریق میشود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
private async Task DeleteImageAsync() { using var context = DbFactory.CreateDbContext(); var image = await context.HotelRoomImages.FindAsync(1); // ... }
روش دوم کار با DbContextFactory در کامپوننتهای Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت
در این روش میتوان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفادهی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننتهای Blazor Server، شبیه به فرمهای برنامههای دسکتاپ عمل میکنند:
@implements IDisposable @inject IDbContextFactory<ApplicationDbContext> DbFactory @code { private ApplicationDbContext Context; protected override async Task OnInitializedAsync() { Context = DbFactory.CreateDbContext(); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { var image = await Context.HotelRoomImages.FindAsync(1); // ... } public void Dispose() { Context.Dispose(); } }
- اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شدهاست. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.
سؤال: اگر سرویسی از ApplicationDbContext تزریق شدهی در سازندهی خود استفاده میکند، چکار باید کرد؟
برای نمونه سرویسهای از پیش تعریف شدهی ASP.NET Core Identity، در سازندهی خود از ApplicationDbContext استفاده میکنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContextهای تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
سؤال: روش پیاده سازی سرویسهای یک برنامه Blazor Server به چه صورتی باید تغییر کند؟
تا اینجا روشهایی که برای استفاده از IDbContextFactory معرفی شدند (که روشهای رسمی و توصیه شدهی اینکار نیز هستند)، فرض را بر این گذاشتهاند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننتها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقهی مایکروسافت است و در تمام مثالهای رسمی آن به صورت ضمنی توصیه میشود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویسهای مجزایی، با مسئولیتهای مشخصی انتقال دهیم، روش استفادهی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شدهی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویسهای EF-Core یک برنامهی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویسهای پروژههای Web API، ضروری نیست؛ چون طول عمر Context آنها توسط خود IoC Container مدیریت میشود؛ اما در برنامههای Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژهی سرویسهای برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شدهای را در سازندهی خود میپذیرد، یافته و تعریف اینترفیس آنرا به صورت زیر تغییر میدهیم:
public interface IHotelRoomService : IDisposable { // ... } public interface IHotelRoomImageService : IDisposable { // ... }
public class HotelRoomService : IHotelRoomService { private bool _isDisposed; // ... public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _dbContext.Dispose(); } } finally { _isDisposed = true; } } } }
ب) Dispose دستی تمام سرویسها، در کامپوننتهای مرتبط
در ادامه تمام کامپوننتهایی را که از سرویسهای فوق استفاده میکنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آنها اضافه میکنیم. سپس متد Dispose آنها را جهت فراخوانی متد Dispose سرویسهای فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال میکنیم:
@page "/hotel-room/create" @page "/hotel-room/edit/{Id:int}" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomImageService.Dispose(); HotelRoomService.Dispose(); } }
@page "/hotel-room" @implements IDisposable // ... @code { // ... public void Dispose() { HotelRoomService.Dispose(); } }
مشکل! اینبار خطای dispose شدن context را دریافت میکنیم!
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose' on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances. Object name: 'ApplicationDbContext'.
مشکلی که در اینجا رخ داده این است که سرویسهایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگیهای آنها را به صورت دستی Dispose کردهایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهلهی ابتدایی که اکنون یکی از وابستگیهای آن Dispose شده، در اختیار برنامه قرار میگیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا میکنیم، لیست اتاقها به خوبی نمایش داده میشوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمهی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ میدهد، فراخوانی متد Dispose کامپوننت لیست اتاقها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی میشود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمیشود و از همان وهلهای که بار اول ایجاد شده، استفاده خواهد شد.
بنابراین سؤال اینجا است که چگونه میتوان سیستم تزریق وابستگیها را وادار کرد تا تمام سرویسهای تزریق شدهی به سازندههای سرویسهای HotelRoomService و HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهلههای قبلی استفاده کند؟
پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازندهی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمیشود:
@code { private IHotelRoomImageService HotelRoomImageService; protected override async Task OnInitializedAsync() { HotelRoomImageService = new HotelRoomImageService(DbFactory.CreateDbContext(), mapper); await base.OnInitializedAsync(); } private async Task DeleteImageAsync() { await HotelRoomImageService.DeleteAsync(1); // ... } public void Dispose() { HotelRoomImageService.Dispose(); } }
وادار کردن Blazor Server به وهله سازی مجدد سرویسهای کامپوننتها
بنابراین مشکل ما Singleton رفتار کردن سرویسها، در برنامههای Blazor است. برای مثال در برنامههای Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگهی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمیشود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویسهای یک کامپوننت نیز در نظر گرفته شدهاند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر میدهیم:
@page "/hotel-room" @*@implements IDisposable*@ @*@inject IHotelRoomService HotelRoomService*@ @inherits OwningComponentBase<IHotelRoomService>
چند نکته:
- فقط یکبار به ازای هر کامپوننت میتوان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت میکنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفتهایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا میتوان از خاصیت Service به صورت مستقیم استفاده کرد و یا میتوان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آنرا به خاصیت عمومی Service هدایت کرد:
@code { private IHotelRoomService HotelRoomService => Service;
@page "/preferences" @using Microsoft.Extensions.DependencyInjection @inherits OwningComponentBase @code { private IHotelRoomService HotelRoomService { get; set; } private IHotelRoomImageService HotelRoomImageService { get; set; } protected override void OnInitialized() { HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>(); HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>(); } }
خلاصهی بحث جاری در مورد روش مدیریت DbContext برنامههای Blazor Server:
- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); services.AddScoped<ApplicationDbContext>(serviceProvider => serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- کامپوننتهای برنامه، سرویسهایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آنها دریافت کنند؛ چون در این حالت همواره به همان وهلهای که در ابتدای کار ایجاد شده، میرسیم:
@inject IHotelRoomService HotelRoomService
@inherits OwningComponentBase<IHotelRoomService>
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-19.zip
نحوهی نمایش خطاها در برنامههای Blazor
در حین توسعهی برنامههای Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر میشود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهدهی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ دادهاست اکتفا کرده و گزینهی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد میدهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت میگیرد:
<div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>
نحوهی مدیریت استثناءها در برنامههای Blazor
توصیه شدهاست که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آنها و یا stack-trace آنها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آنها اطلاعات ارزشمندی را از نحوهی عملکرد برنامه بدست آورد.
برخلاف برنامههای ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آنها میتوان مدیریت سراسری خطاهای رخداده را انجام داد، چنین ویژگی در برنامههای Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آنرا تشکیل میدهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامهی Blazor رخدهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده میشود. در اینجا Console Logging Provider، تامین کنندهی پیشفرض سیستم ثبت وقایع برنامههای Blazor است. به همین جهت استثناءهای مدیریت نشدهی برنامه را میتوان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه میشود.
بنابراین اگر در برنامهی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آنرا به سیستم تزریق وابستگیهای برنامه معرفی کنیم، میتوان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده میکند)، منجمله تمام خطاهای رخداده (و مدیریت نشده) مطلع شد و برای مثال آنها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامهی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.
نوشتن یک ILoggerProvider سفارشی جهت ارسال رخدادها برنامهی سمت کلاینت، به یک Web API
برای ارسال تمام وقایع برنامهی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System; using System.Net.Http; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace BlazorWasmTelegramLogger.Client.Logging { public class ClientLoggerProvider : ILoggerProvider { private readonly HttpClient _httpClient; private readonly WebApiLoggerOptions _options; private readonly NavigationManager _navigationManager; public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager) { if (serviceProvider is null) { throw new ArgumentNullException(nameof(serviceProvider)); } if (options is null) { throw new ArgumentNullException(nameof(options)); } _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>(); _options = options.Value; _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); } public void Dispose() { } } }
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگیهای برنامه معرفی کنیم، روش آن به صورت زیر است:
using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public static class ClientLoggerProviderExtensions { public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>(); return builder; } } }
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده میکنید:
public ClientLoggerProvider( IServiceProvider serviceProvider, IOptions<WebApiLoggerOptions> options, NavigationManager navigationManager)
مهمترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت میدهد:
public ILogger CreateLogger(string categoryName) { return new WebApiLogger(_httpClient, _options, _navigationManager); }
using System; using System.Net.Http; using System.Net.Http.Json; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLogger : ILogger { private readonly WebApiLoggerOptions _options; private readonly HttpClient _httpClient; private readonly NavigationManager _navigationManager; public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _options = options ?? throw new ArgumentNullException(nameof(options)); _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager)); } public IDisposable BeginScope<TState>(TState state) => default; public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel; public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } if (formatter is null) { throw new ArgumentNullException(nameof(formatter)); } try { ClientLog log = new() { LogLevel = logLevel, EventId = eventId, Message = formatter(state, exception), Exception = exception?.Message, StackTrace = exception?.StackTrace, Url = _navigationManager.Uri }; _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log); } catch { // don't throw exceptions from the logger } } } }
- متد IsEnabled آن مشخص میکند که چه سطحی از رخدادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت میکند:
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Client.Logging { public class WebApiLoggerOptions { public string LoggerEndpointUrl { set; get; } public LogLevel LogLevel { get; set; } = LogLevel.Information; } }
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "WebApiLogger": { "LogLevel": "Warning", "LoggerEndpointUrl": "/api/logs" } }
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); // … } } }
using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Shared { public class ClientLog { public LogLevel LogLevel { get; set; } public EventId EventId { get; set; } public string Message { get; set; } public string Exception { get; set; } public string StackTrace { get; set; } public string Url { get; set; } } }
در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام میدهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options)); builder.Services.AddLogging(configure => { configure.AddWebApiLogger(); }); await builder.Build().RunAsync(); } } }
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
ایجاد سرویسی برای ارسال لاگهای برنامه به سمت تلگرام
پیش از اینکه کار تکمیل کنترلر api/logs را در برنامهی Web API انجام دهیم، ابتدا در همان برنامهی Web API، سرویسی را برای ارسال لاگهای رسیده به سمت تلگرام، تهیه میکنیم. علت اینکه این قسمت را به برنامهی سمت سرور محول کردهایم، شامل موارد زیر است:
- درست است که میتوان کتابخانههای مرتبط با تلگرام را به برنامهی سیشارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیمتر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آنرا در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامهی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامهی سمت کلاینت به Web API، مزیت لاگ سمت سرور آنرا مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر میکند و صرفا محدود به تلگرام نیست.
برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه میکنیم:
using System; using System.Text; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Shared; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Telegram.Bot; using Telegram.Bot.Types.Enums; namespace BlazorWasmTelegramLogger.Server.Services { public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } } public interface ITelegramBotService { Task SendLogAsync(ClientLog log); } public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); } public async Task SendLogAsync(ClientLog log) { var text = formatMessage(log); if (string.IsNullOrWhiteSpace(text)) { return; } await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown); } private static string formatMessage(ClientLog log) { if (string.IsNullOrWhiteSpace(log.Message)) { return string.Empty; } var sb = new StringBuilder(); sb.Append(toEmoji(log.LogLevel)) .Append(" *") .AppendFormat("{0:hh:mm:ss}", DateTime.Now) .Append("* ") .AppendLine(log.Message); if (!string.IsNullOrWhiteSpace(log.Exception)) { sb.AppendLine() .Append('`') .AppendLine(log.Exception) .AppendLine(log.StackTrace) .AppendLine("`") .AppendLine(); } sb.Append("*Url:* ").AppendLine(log.Url); return sb.ToString(); } private static string toEmoji(LogLevel level) => level switch { LogLevel.Trace => "⬜️", LogLevel.Debug => "🟦", LogLevel.Information => "⬛️️️", LogLevel.Warning => "🟧", LogLevel.Error => "🟥", LogLevel.Critical => "❌", LogLevel.None => "🔳", _ => throw new ArgumentOutOfRangeException(nameof(level), level, null) }; } }
- برای کار با API تلگرام، از کتابخانهی معروف Telegram.Bot استفاده کردهایم که به صورت زیر، وابستگی آن به برنامهی Web API اضافه میشود:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Telegram.Bot" Version="15.7.1" /> </ItemGroup> </Project>
public class TelegramLoggingBotOptions { public string AccessToken { get; set; } public string ChatId { get; set; } }
پس از شروع این بات، ابتدا دستور newbot/ را صادر کنید. سپس یک نام را از شما میپرسد. نام دلخواهی را وارد کنید. در ادامه یک نام منحصربفرد را جهت شناسایی این بات خواهد پرسید. پس از دریافت آن، توکن خود را همانند تصویر فوق، مشاهده میکنید.
- مرحلهی بعد تنظیم ChatId است. نحوهی کار برنامه به این صورت است که پیامها را به این بات سفارشی خود ارسال کرده و این بات، آنها را به کانال اختصاصی ما هدایت میکند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان میکند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کردهایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظهای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن میتوانید مشاهده کنید:
در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامهی شما است.
در آخر این اطلاعات را در فایل Server\appsettings.json قرار میدهیم:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "TelegramLoggingBot": { "AccessToken": "1826…", "ChatId": "-1001…" } }
namespace BlazorWasmTelegramLogger.Server { public class Startup { // ... public void ConfigureServices(IServiceCollection services) { services.Configure<TelegramLoggingBotOptions>(options => Configuration.GetSection("TelegramLoggingBot").Bind(options)); services.AddSingleton<ITelegramBotService, TelegramBotService>(); // ... } // ... } }
public class TelegramBotService : ITelegramBotService { private readonly string _chatId; private readonly TelegramBotClient _client; public TelegramBotService(IOptions<TelegramLoggingBotOptions> options) { _chatId = options.Value.ChatId; _client = new TelegramBotClient(options.Value.AccessToken); }
ایجاد کنترلر Logs، جهت دریافت لاگهای رسیدهی از سمت کلاینت
مرحلهی آخر کار بسیار سادهاست. سرویس تکمیل شدهی ITelegramBotService را به سازندهی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آنرا فراخوانی میکنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System; using System.Threading.Tasks; using BlazorWasmTelegramLogger.Server.Services; using BlazorWasmTelegramLogger.Shared; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace BlazorWasmTelegramLogger.Server.Controllers { [ApiController] [Route("api/[controller]")] public class LogsController : ControllerBase { private readonly ILogger<LogsController> _logger; private readonly ITelegramBotService _telegramBotService; public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _telegramBotService = telegramBotService; } [HttpPost] public async Task<IActionResult> PostLog(ClientLog log) { // TODO: Save the client's `log` in the database _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message); await _telegramBotService.SendLogAsync(log); return Ok(); } } }
آزمایش برنامه
برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار دادهایم:
@page "/counter" <h1>Counter</h1> <p>Current count: @currentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> @code { private int currentCount = 0; private void IncrementCount() { currentCount++; throw new InvalidOperationException("This is an exception message from the client!"); } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmTelegramLogger.zip