<%: CurrentView %> view | <a href="<%: SwitchUrl %>" data-ajax="false">Switch to <%: AlternateView %></a>
public class ProductViewModel { public int ProductId { get; set; } public string Name { get; set; } public string Description { get; set; } public string ImageUrl { get; set; } public decimal UnitPrice { get; set; } }
public record ProductDTO(int Id, string Name, string Description);
public record ProductDTO { public int Id { get; init; } public string Name { get; init; } } var dto = new ProductDTO { Id = 1, Name = "some name" };
public class Product : DataObject<Product> { public Product(int id) { Id = id; InitializeFromDatabase(); } private void InitializeFromDatabase() { DataHelpers.LoadFromDatabase(this); } public int Id { get; private set; } // other properties and methods }
public class Product { public Product(int id) { Id = id; } private Product() { // required for EF } public int Id { get; private set; } // other properties and methods }
- برای اجرای وظایف خود به فریم ورک ثالثی وابسته نیست.
- به کلاس پایهای ( Base class) نیاز ندارد.
- وابستگی به متد استاتیکی ندارد.
- می تواند در هر جایی از پروژه، نمونه سازی شود.
- اصل Persistence Ignorant را بیشتر رعایت کرده، نه بطور کامل؛ چون یک سازنده دارد که به کتابخانهی ثالثی نیازمند است (سازندهی بدون پارامتر که مورد نیاز EF میباشد).
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/PersianDatePicker.css" rel="stylesheet" /> </head> <body> @RenderBody() <script src="~/Scripts/jquery-1.10.2.min.js"></script> <script src="~/Scripts/PersianDatePicker.js"></script> @RenderSection("Scripts", required: false) </body> </html>
@using System.Globalization @model DateTime? @{ Func<DateTime, string> toPersianDate = date => { var dateTime = new DateTime(date.Year, date.Month, date.Day, new GregorianCalendar()); var persianCalendar = new PersianCalendar(); return persianCalendar.GetYear(dateTime) + "/" + persianCalendar.GetMonth(dateTime).ToString("00") + "/" + persianCalendar.GetDayOfMonth(dateTime).ToString("00"); }; var today = toPersianDate(DateTime.Now); var name = this.ViewContext.ViewData.ModelMetadata.PropertyName; var value = Model.HasValue ? toPersianDate(Model.Value) : string.Empty; } <input type="text" dir="ltr" name="@name" id="@name" value="@value" onclick="PersianDatePicker.Show(this,'@today');" />
// Location: Views\Shared\EditorTemplates\PersianDatePicker.cshtml [UIHint("PersianDatePicker")] public DateTime AddDate { set; get; }
<div> <label>تاریخ:</label> @Html.EditorFor(model => model.AddDate) </div>
همچنین اگر نیاز است مقدار رشته تاریخ شمسی آن به یک DateTime میلادی انتساب داده شود (در حین ارسال اطلاعات به سرور)، باید از یک model binder سفارشی برای اینکار در ASP.NET MVC استفاده کنید. یک نمونه پیاده سازی شده آنرا در بحث PersianDateModelBinder میتوانید مشاهده و استفاده کنید.
SignalR
Async library for .NET to help build real-time, multi-user interactive web applications.
The real-time web is a set of technologies and practices that enable users to receive information as soon as it is published by its authors, rather than requiring that they or their software check a source periodically for updates.
- تکنولوژی جدید WebSocket (^) که خوشبختانه پشتیبانی کاملی از اون در دات نت 4.5 (چهار نقطه پنج! نه چهار و نیم!) وجود داره. اما تمام مرورگرها و تمام وب سرورها از این تکنولوژی پشتیبانی نمیکنند و تنها برخی نسخههای جدید قابلیت استفاده از آخرین ورژن WebSocket رو دارند که میشه به کروم 16 به بالا و فایرفاکس 11 به بالا و اینترنت اکسپلورر 10 اشاره کرد (برای استفاده از این تکنولوژی در ویندوز نیاز به IIS 8.0 است که متاسفانه فقط در ویندوز 8.0 موجوده):
Chrome 16, Firefox 11 and Internet Explorer 10 are currently the only browsers supporting the latest specification (RFC 6455). - یه روش دیگه Server-sent Events نام داره که دادههای جدید رو به فرم رویدادهای DOM به سمت کلاینت میفرسته(^).
- روش دیگهای که موجوده به Forever Frame معروفه که در این روش یک iframe مخفی درون کد html مسئول تبادل دادههاست. این iframe مخفی بهصورت یک بلاک Chunked (^) به سمت کلاینت فرستاده میشه. این iframe که مسئول رندر دادههای جدید در سمت کلاینت هست ارتباط خودش رو با سرور تا ابد! (برای همین بهش forever میگن) حفظ میکنه. هر وقت رویدادی سمت سرور رخ میده با استفاده از این روش دادهها بهصورت تگهای script به این فریم مخفی فرستاده میشوند و چون مرورگرها محتوای html رو به صورت افزایشی (incrementally) رندر میکنن بنابراین این اسکریپتها بهترتیب زمان دریافت اجرا میشوند. (البته ظاهرا عبارت forever frame در صنعت عکاسی! معروفتره بنابراین در جستجو در زمینه این روش ممکنه کمی مشکل داشته باشین) (^).
- روش آخر که در کتابخونه SignalR ازش استفاده میشه long-polling نام داره. در روش polling معمولی پس از ارسال درخواست توسط کلاینت، سرور بلافاصله نتیجه حاصله رو به سمت کلاینت میفرسته و ارتباط قطع میشه. بنابراین برای دادههای جدید درخواست جدیدی باید به سمت سرور فرستاده بشه که تکرار این روش باعث افزایش شدید بار بر روی سرور و کاهش کارآمدی اون میشه. اما در روش long-polling پس از برقراری ارتباط کلاینت با سرور این ارتباط تا مدت زمان معینی (که توسط یه مقدار تایم اوت مشخص میشه و مقدار پیشفرضش 2 دقیقه است) برقرار میمونه. بنابراین کلاینت میتونه بدون ایجاد مشکلی در کارایی، دادههای جدید رو از سرور دریافت کنه. به این روش در برنامهنویسی وب اصطلاحا برنامهنویسی کامت (Comet Programming) میگن (^ ^).
- کلاس سطح پایین PersistentConnection
- کلاس سطح بالای Hub
PM> Install-Package SignalR.Sample
PM> Install-Package SignalR
Microsoft.Web.Infrastructure Newtonsoft.Json SignalR SignalR.Hosting.AspNet SignalR.Hosting.Common
jquery-1.6.4.js jquery.signalR-0.5.1.js
using SignalR.Hubs; namespace SimpleChatWithSignalR { public class SimpleChat : Hub { public void SendMessage(string message) { Clients.reciveMessage(message); } } }
<input type="text" id="msg" /> <input type="button" value="Send" id="send" /><br /> <textarea id='messages' readonly="true" style="height: 200px; width: 200px;"></textarea> <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-0.5.1.min.js" type="text/javascript"></script> <script src="signalr/hubs" type="text/javascript"></script> <script type="text/javascript"> var chat = $.connection.simpleChat; chat.reciveMessage = function (msg) { $('#messages').val($('#messages').val() + "-" + msg + "\r\n"); }; $.connection.hub.start(); $('#send').click(function () { chat.sendMessage($('#msg').val()); }); </script>
<script src="signalr/hubs" type="text/javascript"></script>
به روز رسانی
در دورهای به نام SignalR در سایت، به روز شدهای این مباحث را میتوانید مطالعه کنید.
عموما در ORMها دو سطح کش میتواند وجود داشته باشد:
الف) سطح اول کش
که نمونه بارز آن در EF Code first استفاده از متد context.Entity.Find است. در بار اول فراخوانی این متد، مراجعهای به بانک اطلاعاتی صورت گرفته تا بر اساس primary key ذکر شده در آرگومان آن، رکورد متناظری بازگشت داده شود. در بار دوم فراخوانی متد Find، دیگر مراجعهای به بانک اطلاعاتی صورت نخواهد گرفت و اطلاعات از سطح اول کش (یا همان Context جاری) خوانده میشود.
بنابراین سطح اول کش در طول عمر یک تراکنش معنا پیدا میکند و به صورت خودکار توسط EF مدیریت میشود.
ب) سطح دوم کش
سطح دوم کش در ORMها طول عمر بیشتری داشته و سراسری است. هدف از آن کش کردن اطلاعات عمومی و پر مصرفی است که در دید تمام کاربران قرار دارد و همچنین تمام کاربران میتوانند به آن دسترسی داشته باشند. بنابراین محدود به یک Context نیست.
عموما پیاده سازی سطح دوم کش خارج از ORM مورد استفاده قرار میگیرد و توسط اشخاص و شرکتهای ثالث تهیه میشود.
در حال حاضر پیاده سازی توکاری از سطح دوم کش در EF Code first وجود ندارد و قصد داریم در مطلب جاری به یک پیاده سازی نسبتا خوب از آن برسیم.
تلاشهای صورت گرفته
تا کنون دو پیاده سازی نسبتا خوب از سطح دوم کش در EF صورت گرفته:
Entity Framework Code First Caching
Caching the results of LINQ queries
مورد اول برای ایده گرفتن خوب است. بحث اصلی پیاده سازی سطح دوم کش، یافتن کلیدی است که معادل کوئری LINQ در حال فراخوانی است. سطح دوم کش را به صورت یک Dictionary تصور کنید. هر آیتم آن تشکیل شده است از یک کلید و یک مقدار. از کلید برای یافتن مقدار متناظر استفاده میشود.
اکنون مشکل چیست؟ در یک برنامه ممکن است صدها کوئری لینک وجود داشته باشد. چطور باید به ازای هر کوئری LINQ یک کلید منحصربفرد تولید کرد؟
در مطلب «Entity Framework Code First Caching» از متد ToString استفاده شده است. اگر این متد، بر روی یک عبارت LINQ در EF Code first فراخوانی شود، معادل SQL آن نمایش داده میشود. بنابراین یک قدم به تولید کلید منحصربفرد متناظر با یک کوئری نزدیک شدهایم. اما ... مشکل اینجا است که متد ToString پارامترها را لحاظ نمیکند. بنابراین این روش اصلا قابل استفاده نیست. چون کاربر به ازای تمام پارامترهای ارسالی، همواره یک نتیجه را دریافت خواهد کرد.
در مقاله «Caching the results of LINQ queries» این مشکل برطرف شده است. با parse کامل expression tree یک عبارت LINQ کلید منحصربفرد معادل آن یافت میشود. سپس بر این اساس میتوان نتیجه کوئری را به نحو صحیحی کش کرد. در این روش پارامترها هم لحاظ میشوند و مشکل مقاله قبلی را ندارد.
اما این مقاله دوم یک مشکل مهم را به همراه دارد: روشی را برای حذف آیتمها از کش ارائه نمیدهد. فرض کنید مقالات سایت را در سطح دوم کش قرار دادهاید. اکنون یک مقاله جدید در سایت ثبت شده است. اصطلاحا برای invalidating کش در این روش، راهکاری پیشنهاد نشده است.
پیاده سازی بهتری از سطح دوم کش در EF Code fist
میتوان از همان روش یافتن کلید منحصربفرد معادل با یک کوئری LINQ، که در مقاله دوم فوق، یاد شد، کار را شروع کرد و سپس آنرا به مرحلهای رساند که مباحث حذف کش نیز به صورت خودکار مدیریت شود. پیاده سازی آن را برای برنامههای وب در ذیل ملاحظه میکنید:
using System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Objects; using System.Diagnostics; using System.Linq; using System.Web; using System.Web.Caching; namespace EfSecondLevelCaching.Core { public static class EfHttpRuntimeCacheProvider { #region Methods (6) // Public Methods (2) public static IList<TEntity> ToCacheableList<TEntity>( this IQueryable<TEntity> query, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { return query.Cacheable(x => x.ToList(), durationMinutes, priority); } /// <summary> /// Returns the result of the query; if possible from the cache, otherwise /// the query is materialized and the result cached before being returned. /// The cache entry has a one minute sliding expiration with normal priority. /// </summary> public static TResult Cacheable<TEntity, TResult>( this IQueryable<TEntity> query, Func<IQueryable<TEntity>, TResult> materializer, int durationMinutes = 15, CacheItemPriority priority = CacheItemPriority.Normal) { // Gets a cache key for a query. var queryCacheKey = query.GetCacheKey(); // The name of the cache key used to clear the cache. All cached items depend on this key. var rootCacheKey = typeof(TEntity).FullName; // Try to get the query result from the cache. printAllCachedKeys(); var result = HttpRuntime.Cache.Get(queryCacheKey); if (result != null) { debugWriteLine("Fetching object '{0}__{1}' from the cache.", rootCacheKey, queryCacheKey); return (TResult)result; } // Materialize the query. result = materializer(query); // Adding new data. debugWriteLine("Adding new data: queryKey={0}, dependencyKey={1}", queryCacheKey, rootCacheKey); storeRootCacheKey(rootCacheKey); HttpRuntime.Cache.Insert( key: queryCacheKey, value: result, dependencies: new CacheDependency(null, new[] { rootCacheKey }), absoluteExpiration: DateTime.Now.AddMinutes(durationMinutes), slidingExpiration: Cache.NoSlidingExpiration, priority: priority, onRemoveCallback: null); return (TResult)result; } /// <summary> /// Call this method in `public override int SaveChanges()` of your DbContext class /// to Invalidate Second Level Cache automatically. /// </summary> public static void InvalidateSecondLevelCache(this DbContext ctx) { var changedEntityNames = ctx.ChangeTracker .Entries() .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted) .Select(x => ObjectContext.GetObjectType(x.Entity.GetType()).FullName) .Distinct() .ToList(); if (!changedEntityNames.Any()) return; printAllCachedKeys(); foreach (var item in changedEntityNames) { item.removeEntityCache(); } printAllCachedKeys(); } // Private Methods (4) private static void debugWriteLine(string format, params object[] args) { if (!Debugger.IsAttached) return; Debug.WriteLine(format, args); } private static void printAllCachedKeys() { if (!Debugger.IsAttached) return; debugWriteLine("Available cached keys list:"); int count = 0; var enumerator = HttpRuntime.Cache.GetEnumerator(); while (enumerator.MoveNext()) { if (enumerator.Key.ToString().StartsWith("__")) continue; // such as __System.Web.WebPages.Deployment debugWriteLine("queryKey: {0}", enumerator.Key.ToString()); count++; } debugWriteLine("count: {0}", count); } private static void removeEntityCache(this string rootCacheKey) { if (string.IsNullOrWhiteSpace(rootCacheKey)) return; debugWriteLine("Removing items with dependencyKey={0}", rootCacheKey); // Removes all cached items depend on this key. HttpRuntime.Cache.Remove(rootCacheKey); } private static void storeRootCacheKey(string rootCacheKey) { // The cacheKeys of a cacheDependency that are not already in cache ARE NOT inserted into the cache // on the Insert of the item in which the dependency is used. if (HttpRuntime.Cache.Get(rootCacheKey) != null) return; HttpRuntime.Cache.Add( rootCacheKey, rootCacheKey, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, null); } #endregion Methods } }
توضیحات کدهای فوق
در اینجا یک متدالحاقی به نام Cacheable توسعه داده شده است که میتواند در انتهای کوئریهای LINQ شما قرار گیرد. مثلا:
var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault());
کاری که در این متد انجام میشود به این شرح است:
الف) ابتدا کلید منحصربفرد معادل کوئری LINQ فراخوانی شده محاسبه میشود.
ب) بر اساس نام کامل نوع Entity در حال استفاده، کلید دیگری به نام rootCacheKey تولید میگردد.
شاید بپرسید اهمیت این کلید چیست؟
فرض کنید در حال حاضر 1000 آیتم در کش وجود دارند. چه روشی را برای حذف آیتمهای مرتبط با کش Entity1 پیشنهاد میدهید؟ احتمالا خواهید گفت تمام کش را بررسی کرده و آیتمها را یکی یکی حذف میکنیم.
این روش بسیار کند است (و جواب هم نمیدهد؛ چون کلیدی که در اینجا تولید شده، هش MD5 معادل کوئری است و نمیتوان آنرا به موجودیتی خاص ربط داد) و ... نکته جالبی در متد HttpRuntime.Cache.Insert برای مدیریت آن پیش بینی شده است: استفاده از CacheDependency.
توسط CacheDependency میتوان گروهی از آیتمهای همخانواده را تشکیل داد. سپس برای حذف کل این گروه کافی است کلید اصلی CacheDependency را حذف کرد. به این ترتیب به صورت خودکار کل کش مرتبط خالی میشود.
ج) مراحل بعدی آن هم یک سری اعمال متداول هستند. ابتدا توسط HttpRuntime.Cache.Get بررسی میشود که آیا بر اساس کلید متناظر با کوئری جاری، اطلاعاتی در کش وجود دارد یا خیر. اگر بله، نتیجه از کش خوانده میشود. اگر خیر، کوئری اصطلاحا materialized میشود تا بر روی بانک اطلاعاتی اجرا شده و نتیجه بازگشت داده شود. سپس این نتیجه را در کش قرار میدهیم.
مورد بعدی که باید به آن دقت داشت، خالی کردن کش، پس از به روز رسانی اطلاعات توسط کاربران است. این کار در متد InvalidateSecondLevelCache صورت میگیرد. به کمک ChangeTracker میتوان نام نوعهای موجودیتهای تغییر کرده را یافت. چون کلید اصلی CacheDependency را بر مبنای همین نام نوعهای موجودیتها تعیین کردهایم، به سادگی میتوان کش مرتبط با موجودیت یافت شده را خالی کرد.
استفاده از متد InvalidateSecondLevelCache یاد شده به نحو زیر است:
using System.Data.Entity; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.Models; namespace EfSecondLevelCaching.Test.DataLayer { public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public override int SaveChanges() { this.InvalidateSecondLevelCache(); return base.SaveChanges(); } } }
در اینجا با تحریف متد SaveChanges، میتوان درست در زمان اعمال تغییرات به بانک اطلاعاتی، قسمتی از کش را غیرمعتبر کرد.
نحوه استفاده از سطح دوم کش توسعه داده شده
مثالی از کاربرد متدهای الحاقی توسعه داده شده را در ذیل مشاهده میکنید:
using System.Data.Entity; using System.Linq; using EfSecondLevelCaching.Core; using EfSecondLevelCaching.Test.DataLayer; using EfSecondLevelCaching.Test.Models; using System; namespace EfSecondLevelCaching { public static class TestUsages { public static void RunQueries() { using (ProductContext context = new ProductContext()) { var isActive = true; var name = "Product1"; // reading from db var list1 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list2 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from cache var list3 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == name) .ToCacheableList(); // reading from db var list4 = context.Products .OrderBy(one => one.ProductNumber) .Where(x => x.IsActive == isActive && x.ProductName == "Product2") .ToCacheableList(); } // removes products cache using (ProductContext context = new ProductContext()) { var p = new Product() { IsActive = false, ProductName = "P4", ProductNumber = "004" }; context.Products.Add(p); context.SaveChanges(); } using (ProductContext context = new ProductContext()) { var data = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); var data2 = context.Products.AsQueryable().Cacheable(x => x.FirstOrDefault()); context.SaveChanges(); } } } }
در این حالت اگر برنامه را اجرا کنیم به یک چنین خروجی در پنجره Debug ویژوال استودیو خواهیم رسید:
Adding new data: queryKey=72AF5DA1BA9B91E24DCCF83E88AD1C5F, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__72AF5DA1BA9B91E24DCCF83E88AD1C5F' from the cache. Available cached keys list: queryKey: EfSecondLevelCaching.Test.Models.Product queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F count: 2 Adding new data: queryKey=11A2C33F9AD7821A0A31003BFF1DF886, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 72AF5DA1BA9B91E24DCCF83E88AD1C5F queryKey: 11A2C33F9AD7821A0A31003BFF1DF886 queryKey: EfSecondLevelCaching.Test.Models.Product count: 3 Removing items with dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: count: 0 Available cached keys list: count: 0 Adding new data: queryKey=02E6FE403B461E45C5508684156C1D10, dependencyKey=EfSecondLevelCaching.Test.Models.Product Available cached keys list: queryKey: 02E6FE403B461E45C5508684156C1D10 queryKey: EfSecondLevelCaching.Test.Models.Product count: 2 Fetching object 'EfSecondLevelCaching.Test.Models.Product__02E6FE403B461E45C5508684156C1D10' from the cache.
توضیحات:
در زمان تولید list1 چون اطلاعاتی در کش سطح دوم وجود ندارد، پیغام Adding new data قابل مشاهده است. اطلاعات از بانک اطلاعاتی دریافت شده و سپس در کش قرار داده میشود.
حین فراخوانی list2 که دقیقا همان کوئری list1 را یکبار دیگر فراخوانی میکند، به عبارت Fetching object خواهیم رسید که بر دریافت اطلاعات از کش سطح دوم بجای مراجعه به بانک اطلاعاتی دلالت دارد.
در list4 چون پارامترهای کوئری تغییر کردهاند، بنابراین دیگر کلید منحصربفرد معادل آن با list1 و lis2 یکی نیست و اینبار پیغام Adding new data مشاهده میشود؛ چون برای دریافت اطلاعات آن نیاز است که به بانک اطلاعاتی مراجعه شود.
در ادامه یک context دیگر باز شده و در آن رکوردی به بانک اطلاعاتی اضافه میشود. به همین دلیل اینبار پیام Removing items with dependencyKey قابل مشاهده است. به عبارتی متد InvalidateSecondLevelCache وارد عمل شده است و بر اساس تغییری که صورت گرفته، کش را غیرمعتبر کرده است.
سپس در context بعدی تعریف شده، دوبار متد FirstOrDefault فراخوانی شده است. اولین مورد Adding new data است و دومین فراخوانی به Fetching object ختم شده است (دریافت اطلاعات از کش).
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید:
EfSecondLevelCaching.zip
ایجاد کامپوننت جدید BlazorJS
برای بررسی نحوهی تعامل جاوا اسکریپت و Blazor، در ابتدا کامپوننت جدید Pages\LearnBlazor\BlazorJS.razor را ایجاد کرده:
@page "/BlazorJS" <h3>BlazorJS</h3> @code { }
<li class="nav-item px-3"> <NavLink class="nav-link" href="BlazorJS"> <span class="oi oi-list-rich" aria-hidden="true"></span> BlazorJS </NavLink> </li>
روش فراخوانی کدهای جاوا اسکریپتی از طریق کدهای سیشارپ Blazor
فرض کنید میخواهیم در حین کلیک بر روی دکمهای مانند دکمهی حذف، ابتدا تائیدیهای را توسط تابع confirm جاوا اسکریپتی، از کاربر اخذ کنیم. روش انجام چنین کاری در برنامههای مبتنی بر Blazor به صورت زیر است:
@page "/BlazorJS" @inject IJSRuntime JsRuntime <h3>BlazorJS</h3> <div class="row"> <button class="btn btn-secondary" @onclick="TestConfirmBox">Test Confirm Button</button> </div> <div class="row"> @if (ConfirmResult) { <p>Confirmation has been made!</p> } else { <p>Confirmation Pending!</p> } </div> @code { string ConfirmMessage = "Are you sure you want to click?"; bool ConfirmResult; async Task TestConfirmBox() { ConfirmResult = await JsRuntime.InvokeAsync<bool>("confirm", ConfirmMessage); } }
- در اینجا میخواهیم تابع استاندارد confirm جاوا اسکریپتی را از طریق کدهای سیشارپ، با کلیک بر روی دکمهی Test Confirm Button، فراخوانی کنیم. به همین جهت onclick@ این دکمه، به متد TestConfirmBox کدهای UI سیشارپ این کامپوننت، متصل شدهاست.
- برای دسترسی به توابع جاوا اسکریپتی، نیاز است سرویس توکار IJSRuntime را به کدهای کامپوننت تزریق کنیم که روش انجام آنرا توسط دایرکتیو inject@ مشاهده میکنید. برای دسترسی به این سرویس توکار، نیاز به تنظیمات ابتدایی خاصی نیست و اینکار پیشتر انجام شدهاست.
- سرویس JsRuntime تزریق شده، دو متد مهم InvokeVoidAsync و InvokeAsync را جهت فراخوانی توابع جاوا اسکریپتی به همراه دارد. اگر تابعی، خروجی غیر void داشته باشد، باید از متد InvokeAsync استفاده کرد. برای مثال خروجی تابع استاندارد confirm، از نوع boolean است. بنابراین نوع این خروجی را به صورت یک آرگومان جنریک متد InvokeAsync مشخص کردهایم.
- اولین پارامتر متد InvokeAsync، نام رشتهای تابع جاوا اسکریپتی است که قرار است صدا زده شود. پارامترهای اختیاری بعدی که به صورت params object?[]? args تعریف شدهاند، لیست نامحدود آرگومانهای ورودی این متد هستند.
- فیلد ConfirmMessage، پیامی را جهت اخذ تائید، تعریف میکند که به عنوان پارامتر متد confirm، توسط JsRuntime.InvokeAsync فراخوانی خواهد شد.
- فیلد ConfirmResult، نتیجهی فراخوانی متد confirm جاوا اسکریپتی را به همراه دارد.
- در اینجا روش عکس العمل نشان دادن به خروجی دریافتی از متد جاوااسکریپتی را نیز مشاهده میکنید. پس از پایان متد TestConfirmBox که یک متد رویدادگران است، همانطور که در مطلب بررسی «چرخهی حیات کامپوننتها» نیز بررسی کردیم، متد StateHasChanged، در پشت صحنه فراخوانی میشود که سبب رندر مجدد UI خواهد شد. بنابراین در حین رندر مجدد UI، بر اساس مقدار جدید ConfirmResult دریافت شدهی از کاربر، با تشکیل یک if/else@، میتوان به نتیجهی تائید یا عدم تائید کاربر، واکنش نشان داد. با این توضیحات در اولین بار نمایش کامپوننت جاری چون مقدار ConfirmResult مساوی false است، پیام زیر را مشاهده میکنیم:
اما در ادامه با کلیک بر روی دکمه و تائید پیام ظاهر شده، عبارت زیر ظاهر میشود:
روش افزودن یک کتابخانهی خارجی جاوا اسکریپتی به پروژههای Blazor
فرض کنید میخواهیم پیامهای برنامه را توسط کتابخانهی معروف جاوا اسکریپتی Toastr نمایش دهیم؛ با این دمو.
مرحلهی اول کار با این کتابخانه، دریافت فایلهای CSS و JS آن است. برای این منظور قصد داریم از برنامهی مدیریت بستههای LibMan استفاده کنیم:
dotnet tool install -g Microsoft.Web.LibraryManager.Cli libman init libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap libman install jquery --provider unpkg --destination wwwroot/lib/jquery libman install toastr --provider unpkg --destination wwwroot/lib/toastr
البته تعاریف مداخل آنها به فایل libman.json نیز اضافه میشوند. مزیت آن، اجرای دستور libman restore برای بازیابی و نصب مجدد تمام بستههای ذکر شدهی در فایل libman.json است.
پس از دریافت بستههای سمت کلاینت آن، مداخل مرتبط را به فایل Pages\_Host.cshtml برنامهی Blazor Server اضافه خواهیم کرد (و یا در فایل wwwroot/index.html برنامههای Blazor WASM).
<head> <base href="~/" /> <link rel="stylesheet" href="lib/toastr/build/toastr.min.css" /> </head> <body> <script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/toastr/build/toastr.min.js"></script> <script src="_framework/blazor.server.js"></script> </body>
یک نکته: میتوان فایل csproj برنامه را به صورت زیر تغییر داد تا کار اجرای دستور libman restore را قبل از build، به صورت خودکار انجام دهد:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> </PropertyGroup> <Target Name="DebugEnsureLibManEnv" BeforeTargets="BeforeBuild" Condition=" '$(Configuration)' == 'Debug' "> <!-- Ensure libman is installed --> <Exec Command="libman --version" ContinueOnError="true"> <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> </Exec> <Error Condition="'$(ErrorCode)' != '0'" Text="libman is required to build and run this project. To continue, please run `dotnet tool install -g Microsoft.Web.LibraryManager.Cli`, and then restart your command prompt or IDE." /> <Message Importance="high" Text="Restoring dependencies using 'libman'. This may take several minutes..." /> <Exec WorkingDirectory="$(MSBuildProjectDirectory)" Command="libman restore" /> </Target> </Project>
روش فراخوانی یک کتابخانهی خارجی جاوا اسکریپتی در پروژههای Blazor
پس از افزودن فایلهای سمت کلاینت toastr و تعریف مداخل آن در فایل Pages\_Host.cshtml برنامهی Blazor Server جاری، اکنون میخواهیم از این کتابخانه استفاده کنیم. یک روش کار با این نوع کتابخانههای عمومی و سراسری به صورت زیر است:
- ابتدا فایل خالی جدید wwwroot\js\common.js را ایجاد میکنیم.
- سپس تابع عمومی و سراسری ShowToastr را بر اساس امکانات کتابخانهی toastr و مستندات آن، به صورت زیر ایجاد میکنیم:
window.ShowToastr = (type, message) => { // Toastr don't work with Bootstrap 4.2 toastr.options.toastClass = "toastr"; // https://github.com/CodeSeven/toastr/issues/599 if (type === "success") { toastr.success(message, "Operation Successful", { timeOut: 20000 }); } if (type === "error") { toastr.error(message, "Operation Failed", { timeOut: 20000 }); } };
سطر اول آن هم برای رفع عدم تداخل با بوت استرپ 4x اضافه شدهاست. بوت استرپ 4x به همراه کلاسهای CSS مشابهی است که نیاز است با تنظیم toastClass به مقداری دیگر، این تداخل را برطرف کرد.
- در ادامه مدخل تعریف فایل wwwroot\js\common.js را به انتهای تگ body فایل Pages\_Host.cshtml اضافه میکنیم:
<script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/toastr/build/toastr.min.js"></script> <script src="js/common.js"></script> <script src="_framework/blazor.server.js"></script> </body>
در آخر برای آزمایش آن به کامپوننت Pages\LearnBlazor\BlazorJS.razor مراجعه کرده و تابع سراسری ShowToastr را دقیقا مانند روشی که در مورد تابع confirm بکار بردیم، توسط سرویس JsRuntime، فراخوانی میکنیم:
@page "/BlazorJS" @inject IJSRuntime JsRuntime <div class="row"> <button class="btn btn-success" @onclick="@(()=>TestSuccess("Success Message"))">Test Toastr Success</button> <button class="btn btn-danger" @onclick="@(()=>TestFailure("Error Message"))">Test Toastr Failure</button> </div> @code { async Task TestSuccess(string message) { await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message); } async Task TestFailure(string message) { await JsRuntime.InvokeVoidAsync("ShowToastr", "error", message); } }
کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی
میتوان جهت کاهش تکرار کدهای استفاده از تابع ShowToastr، متدهای الحاقی زیر را برای سرویس IJSRuntime تهیه کرد:
using System.Threading.Tasks; using Microsoft.JSInterop; namespace BlazorServerSample.Utils { public static class JSRuntimeExtensions { public static ValueTask ToastrSuccess(this IJSRuntime JSRuntime, string message) { return JSRuntime.InvokeVoidAsync("ShowToastr", "success", message); } public static ValueTask ToastrError(this IJSRuntime JSRuntime, string message) { return JSRuntime.InvokeVoidAsync("ShowToastr", "error", message); } } }
@using BlazorServerSample.Utils
async Task TestSuccess(string message) { //await JsRuntime.InvokeVoidAsync("ShowToastr", "success", message); await JsRuntime.ToastrSuccess(message); }
فراخوانی یک متد عمومی واقع در کامپوننت فرزند از طریق کامپوننت والد
فرض کنید در کامپوننت فرزند Pages\LearnBlazor\LearnBlazorComponents\ChildComponent.razor که در قسمتهای قبل آنرا تکمیل کردیم، متد عمومی زیر تعریف شدهاست:
@inject IJSRuntime JsRuntime @code { // ... public async Task TestSuccess(string message) { await JsRuntime.ToastrSuccess(message); } }
@page "/ParentComponent" <ChildComponent OnClickBtnMethod="ShowMessage" @ref="ChildComp" Title="This title is passed as a parameter from the Parent Component"> <ChildContent> A `Render Fragment` from the parent! </ChildContent> <DangerChildContent> A danger content from the parent! </DangerChildContent> </ChildComponent> <div class="row"> <button class="btn btn-success" @onclick="@(()=>ChildComp.TestSuccess("Done!"))">Show Alert</button> </div> @code { ChildComponent ChildComp; // ... }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-11.zip
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.AspNet.OData" version="6.0.0" targetFramework="net462" /> <package id="Microsoft.AspNet.SignalR.Core" version="2.2.1" targetFramework="net462" /> <package id="Microsoft.AspNet.SignalR.Owin" version="1.2.2" targetFramework="net462" /> <package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net462" /> <package id="Microsoft.AspNet.WebApi.Core" version="5.2.3" targetFramework="net462" /> <package id="Microsoft.AspNet.WebApi.Owin" version="5.2.3" targetFramework="net462" /> <package id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="1.0.2" targetFramework="net462" /> <package id="Microsoft.Extensions.DependencyInjection" version="1.0.0" targetFramework="net462" /> <package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="1.0.0" targetFramework="net462" /> <package id="Microsoft.Net.Compilers" version="1.3.2" targetFramework="net462" developmentDependency="true" /> <package id="Microsoft.OData.Core" version="7.0.0" targetFramework="net462" /> <package id="Microsoft.OData.Edm" version="7.0.0" targetFramework="net462" /> <package id="Microsoft.Owin" version="3.0.1" targetFramework="net462" /> <package id="Microsoft.Owin.FileSystems" version="3.0.1" targetFramework="net462" /> <package id="Microsoft.Owin.Host.SystemWeb" version="3.0.1" targetFramework="net462" /> <package id="Microsoft.Owin.Security" version="3.0.1" targetFramework="net462" /> <package id="Microsoft.Owin.StaticFiles" version="3.0.1" targetFramework="net462" /> <package id="Microsoft.Spatial" version="7.0.0" targetFramework="net462" /> <package id="Newtonsoft.Json" version="9.0.1" targetFramework="net462" /> <package id="Owin" version="1.0" targetFramework="net462" /> <package id="System.Collections" version="4.0.11" targetFramework="net462" /> <package id="System.Collections.Concurrent" version="4.0.12" targetFramework="net462" /> <package id="System.ComponentModel" version="4.0.1" targetFramework="net462" /> <package id="System.Diagnostics.Debug" version="4.0.11" targetFramework="net462" /> <package id="System.Globalization" version="4.0.11" targetFramework="net462" /> <package id="System.Linq" version="4.1.0" targetFramework="net462" /> <package id="System.Linq.Expressions" version="4.1.0" targetFramework="net462" /> <package id="System.Reflection" version="4.1.0" targetFramework="net462" /> <package id="System.Resources.ResourceManager" version="4.0.1" targetFramework="net462" /> <package id="System.Runtime.Extensions" version="4.1.0" targetFramework="net462" /> <package id="System.Threading" version="4.0.11" targetFramework="net462" /> <package id="System.Threading.Tasks" version="4.0.11" targetFramework="net462" /> </packages>
PM>Update-Package -reinstall -Project YourProjectName
<?xml version="1.0" encoding="utf-8"?> <configuration> <appSettings> <add key="owin:AppStartup" value="OwinKatanaTest.OwinAppStartup, OwinKatanaTest" /> <!-- Owin App Startup Class --> <add key="webpages:Enabled" value="false" /> <!-- Disable asp.net web pages. Note that based on our current configuration, asp.net web forms, mvc and web pages won't work. This configuration is for owin stuffs only, for example asp.net web api & odata, signalr, etc. --> </appSettings> <system.web> <compilation debug="true" defaultLanguage="c#" enablePrefetchOptimization="true" optimizeCompilations="true" targetFramework="4.6.2"> <assemblies> <remove assembly="*" /> <!-- To improve app startup performance, our app will continue its work without this compilations, these are required for asp.net web forms, mvc and web pages. --> <add assembly="OwinKatanaTest" /> </assemblies> </compilation> <httpRuntime targetFramework="4.6.2" /> <httpModules> <!-- No need to these modules and handlers, owin handler itself will do everything for us --> <clear /> </httpModules> <httpHandlers> <clear /> </httpHandlers> <sessionState mode="Off" /> </system.web> <system.codedom> <compilers> <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.2.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:6 /nowarn:1659;1699;1701" /> </compilers> </system.codedom> <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Microsoft.AspNet.SignalR.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-2.2.1.0" newVersion="2.2.1.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Runtime.Extensions" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" /> </dependentAssembly> </assemblyBinding> </runtime> <system.webServer> <validation validateIntegratedModeConfiguration="false" /> <modules runAllManagedModulesForAllRequests="false"> <!-- We're not going to remove all modules, some modules such as static & dynamic compression modules are really cool (-: --> <remove name="RewriteModule" /> <remove name="OutputCache" /> <remove name="Session" /> <remove name="WindowsAuthentication" /> <remove name="FormsAuthentication" /> <remove name="DefaultAuthentication" /> <remove name="RoleManager" /> <remove name="FileAuthorization" /> <remove name="UrlAuthorization" /> <remove name="AnonymousIdentification" /> <remove name="Profile" /> <remove name="UrlMappingsModule" /> <remove name="ServiceModel-4.0" /> <remove name="UrlRoutingModule-4.0" /> <remove name="ScriptModule-4.0" /> <remove name="Isapi" /> <remove name="IsapiFilter" /> <remove name="DigestAuthentication" /> <remove name="WindowsAuthentication" /> <remove name="ServerSideInclude" /> <remove name="DirectoryListing" /> <remove name="DefaultDocument" /> <remove name="CustomError" /> <remove name="Cgi" /> </modules> <defaultDocument> <!-- Default docs will be configured using owin static files middleware --> <files> <clear /> </files> </defaultDocument> <handlers> <!-- Only use this handler for all requests --> <clear /> <add name="Owin" verb="*" path="*" type="Microsoft.Owin.Host.SystemWeb.OwinHttpHandler, Microsoft.Owin.Host.SystemWeb" /> </handlers> <httpProtocol> <customHeaders> <clear /> </customHeaders> </httpProtocol> </system.webServer> </configuration>
بعد از build کردن پروژه، در صورت خطا داشتن از Referencesها، System.Reflection و System.Runtime.Extensions را حذف کنید.
using Microsoft.AspNet.SignalR; using Microsoft.OData; using Microsoft.OData.Edm; using Owin; using OwinKatanaTest.Model; using OwinKatanaTest.ODataControllers; using System.Collections.Generic; using System.Web.Http; using System.Web.OData.Builder; using System.Web.OData.Extensions; using System.Web.OData.Routing.Conventions; namespace OwinKatanaTest { public class OwinAppStartup { public void Configuration(IAppBuilder owinApp) { owinApp.Map("/odata", innerOwinAppForOData => { HttpConfiguration webApiODataConfig = new HttpConfiguration(); webApiODataConfig.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; webApiODataConfig.Formatters.Clear(); IEnumerable<IODataRoutingConvention> conventions = ODataRoutingConventions.CreateDefault(); ODataModelBuilder modelBuilder = new ODataConventionModelBuilder(webApiODataConfig); modelBuilder.Namespace = modelBuilder.ContainerName = "Test"; var categoriesSetConfig = modelBuilder.EntitySet<Category>("Categories"); var getBestCategoryFunctionConfig = categoriesSetConfig.EntityType.Collection.Function(nameof(CategoriesController.GetBestCategory)); getBestCategoryFunctionConfig.ReturnsFromEntitySet<Category>("Categories"); IEdmModel edmModel = modelBuilder.GetEdmModel(); webApiODataConfig.MapODataServiceRoute("default", "", builder => { builder.AddService(ServiceLifetime.Singleton, sp => conventions); builder.AddService(ServiceLifetime.Singleton, sp => edmModel); }); innerOwinAppForOData.UseWebApi(webApiODataConfig); }); owinApp.Map("/api", innerOwinAppForWebApi => { HttpConfiguration webApiConfig = new HttpConfiguration(); webApiConfig.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always; webApiConfig.MapHttpAttributeRoutes(); webApiConfig.Routes.MapHttpRoute(name: "default", routeTemplate: "{controller}/{action}", defaults: new { action = RouteParameter.Optional }); innerOwinAppForWebApi.UseWebApi(webApiConfig); }); owinApp.Map("/signalr", innerOwinAppForSignalR => { innerOwinAppForSignalR.RunSignalR(new HubConfiguration { EnableDetailedErrors = true }); }); owinApp.UseStaticFiles(); owinApp.Run(async context => { await context.Response.WriteAsync("owin katana"); }); } } }
using OwinKatanaTest.Model; using System.Web.Http; using System.Web.OData; namespace OwinKatanaTest.ODataControllers { public class CategoriesController : ODataController { [HttpGet] public Category GetBestCategory() { return new Category { Id = 1, Name = "Test" }; } } }
using OwinKatanaTest.Model; using System.Collections.Generic; using System.Web.Http; namespace OwinKatanaTest.ApiControllers { public class ProductsController : ApiController { [HttpGet] [Route("products/{categoryId}")] public List<Product> GetProductsByCategoryId(int categoryId) { return new List<Product> { new Product { Id = 1 , Name = "Test" } }; } } }
<package id="Microsoft.Owin.Testing" version="3.0.1" targetFramework="net462" />
در ادامه تست خود را اینگونه مینویسیم
using Microsoft.Owin.Testing; using Microsoft.VisualStudio.TestTools.UnitTesting; using OwinKatanaTest; using System.Net; using System.Net.Http; using System.Threading.Tasks; namespace Test { [TestClass] public class Test { [TestMethod] public async Task TestWebApi() { using (TestServer server = TestServer.Create<OwinAppStartup>()) { HttpResponseMessage apiResponse = await server.HttpClient.GetAsync("/api/products/1"); apiResponse.EnsureSuccessStatusCode(); Assert.AreEqual(HttpStatusCode.OK, apiResponse.StatusCode); } } [TestMethod] public async Task TestOData() { using (TestServer server = TestServer.Create<OwinAppStartup>()) { HttpResponseMessage odataResponse = await server.HttpClient.GetAsync("odata/Categories/Test.GetBestCategory"); odataResponse.EnsureSuccessStatusCode(); Assert.AreEqual(HttpStatusCode.OK, odataResponse.StatusCode); } } } }
برای طراحی یک صفحه modal چهار div باید اضافه شوند. بیرونیترین div باید دارای کلاس modal مجموعه Bootstrap باشد. میتوان به کلاس modal در اینجا کلاسهای hide fade را هم برای نمونه اضافه کرد. در این حالت، نمایش و بسته شدن صفحه modal به همراه پویانمایی ویژهای خواهد بود.
داخل این div، سه div با کلاسهای modal-header برای نمایش هدر، modal-body برای نمایش محتوایی در این صفحه modal و modal-footer برای تدارک محتوای footer این صفحه، قرار خواهند گرفت.
در این بین هر لینکی با ویژگی data-dismiss=modal، سبب بسته شدن خودکار صفحه باز شده خواهد شد.
افزونه bootstrapModalConfirm
اگر نکات یاد شده را بخواهیم کپسوله کنیم، میتوان یک افزونه جدید جیکوئری را با نام فایل jquery.bootstrap-modal-confirm.js برای این منظور تدارک دید:
// <![CDATA[ (function ($) { $.bootstrapModalConfirm = function (options) { var defaults = { caption: 'تائید عملیات', body: 'آیا عملیات درخواستی اجرا شود؟', onConfirm: null, confirmText: 'تائید', closeText: 'انصراف' }; var options = $.extend(defaults, options); var confirmContainer = "#confirmContainer"; var html = '<div class="modal hide fade" id="confirmContainer">' + '<div class="modal-header">' + '<a class="close" data-dismiss="modal">×</a>' + '<h5>' + options.caption + '</h5></div>' + '<div class="modal-body">' + options.body + '</div>' + '<div class="modal-footer">' + '<a href="#" class="btn btn-success" id="confirmBtn">' + options.confirmText + '</a>' + '<a href="#" class="btn" data-dismiss="modal">' + options.closeText + '</a></div></div>'; $(confirmContainer).remove(); $(html).appendTo('body'); $(confirmContainer).modal('show'); $('#confirmBtn', confirmContainer).click(function () { if (options.onConfirm) options.onConfirm(); $(confirmContainer).modal('hide'); }); }; })(jQuery); // ]]>
مثالی از استفاده از افزونه bootstrapModalConfirm
@{ ViewBag.Title = "Index"; } <h2> Index</h2> <a href="#" class="btn btn-danger" id="deleteBtn">حذف رکورد</a> @section JavaScript { <script type="text/javascript"> $(function () { $("#deleteBtn").click(function (e) { e.preventDefault(); //میخواهیم لینک به صورت معمول عمل نکند $.bootstrapModalConfirm({ caption: 'تائید عملیات', body: 'آیا عملیات درخواستی اجرا شود؟', onConfirm: function () { alert('در حال انجام عملیات'); }, confirmText: 'تائید', closeText: 'انصراف' }); }); }); </script> }
در پروژه هایی به صورت Smart UI کد نویسی شده اند و یا حتی قصد انجام پروژه با تکنولوژیهای WPF یا Windows Application را دارید و نیاز دارید که فرمهای خود را به صورت generic بسازید این مقاله به شما کمک خواهد کرد.
#Windows Application
یک پروژه از نوع Windows Application ایجاد میکنیم و یک فرم به نام FrmBase در آن خواهیم داشت. یک Label در فرم قرار دهید و مقدار Text آن را فرم اصلی قرار دهید.
در فرم مربوطه، فرم را به صورت generic تعریف کنید. به صورت زیر:
public partial class FrmBase<T> : Form where T : class { public FrmBase() { InitializeComponent(); } }
بعد باید همین تغییرات را در فایل FrmBase.designer.cs هم اعمال کنیم:
partial class FrmBase<T> where T : class { /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.IContainer components = null; /// <summary> /// Clean up any resources being used. /// </summary> /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> protected override void Dispose( bool disposing ) { if ( disposing && ( components != null ) ) { components.Dispose(); } base.Dispose( disposing ); } #region Windows Form Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { this.label1 = new System.Windows.Forms.Label(); this.SuspendLayout(); // // label1 // this.label1.AutoSize = true; this.label1.Location = new System.Drawing.Point(186, 22); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(51, 13); this.label1.TabIndex = 0; this.label1.Text = "فرم اصلی"; // // FrmBase // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(445, 262); this.Controls.Add(this.label1); this.Name = "FrmBase"; this.Text = "Form1"; this.ResumeLayout(false); this.PerformLayout(); } #endregion private System.Windows.Forms.Label label1; }
public partial class FrmTest : FrmBase<String> { public FrmTest() { InitializeComponent(); } }
با این که برنامه به راحتی اجرا میشود و خروجی آن قابل مشاهده است ولی امکان نمایش فرم در حالت design وجود ندارد. متاسفانه در Windows Appها برای تعریف فرمها به صورت generic یا این مشکل روبرور هستیم. تنها راه موجود برای حل این مشکل استفاده از یک کلاس کمکی است. به صورت زیر:
public partial class FrmTest : FrmTestHelp { public FrmTest() { InitializeComponent(); } } public class FrmTestHelp : FrmBase<String> { }
#WPF
در پروژههای WPF ، راه حلی برای این مشکل در نظر گرفته شده است. در WPF، برای Window یا UserControl پایه نمیتوان Designer داشت. ابتدا باید فرم پایه را به صورت زیر ایجاد کنیم:
public class WindowBase<T> : Window where T : class { }
public partial class MainWindow: WindowBase<String> { public MainWindow() { InitializeComponent(); } }
<local:WindowBase x:Class="GenericWindows.MainWindow"
x:TypeArguments="sys:String" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:GenericWindows" xmlns:sys="clr-namespace:System;assembly=mscorlib" Title="MainWindow" Height="350" Width="525"> <Grid> </Grid> </local:WindowBase>
<div class="alert"> نمایش اعلانات </div>
تعدادی کلاس دیگر نیز جهت استفاده از رنگهای مختلف نیز توسط بوت استرپ ارائه شده است:
همچنین اگر مایل بودید میتوانید با افزودن یک دکمه با کلاس close و ویژگی data-dismiss مساوی alert، امکان بستن پیام را در اختیار کاربر قرار دهید:
<div class="alert alert-warning alert-dismissable"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> نمایش اعلان </div>
در ادامه قصد داریم این پیام را بعد از ثبت اطلاعات، به کاربر نمایش دهیم. یعنی در داخل کد، امکان صدا زدن این نوع پیامها را داشته باشیم.
ابتدا کلاسهای زیر را تعریف میکنیم:
کلاس Alert
public class Alert { public const string TempDataKey = "TempDataAlerts"; public string AlertStyle { get; set; } public string Message { get; set; } public bool Dismissable { get; set; } }
در کلاس فوق خصوصیت یک alert را تعریف کردهایم (از خاصیت TempDataKey جهت پاس دادن alertها به view استفاده میکنیم).
public class AlertStyles { public const string Success = "success"; public const string Information = "info"; public const string Warning = "warning"; public const string Danger = "danger"; }
public class BaseController : Controller { public void Success(string message, bool dismissable = false) { AddAlert(AlertStyles.Success, message, dismissable); } public void Information(string message, bool dismissable = false) { AddAlert(AlertStyles.Information, message, dismissable); } public void Warning(string message, bool dismissable = false) { AddAlert(AlertStyles.Warning, message, dismissable); } public void Danger(string message, bool dismissable = false) { AddAlert(AlertStyles.Danger, message, dismissable); } private void AddAlert(string alertStyle, string message, bool dismissable) { var alerts = TempData.ContainsKey(Alert.TempDataKey) ? (List<Alert>)TempData[Alert.TempDataKey] : new List<Alert>(); alerts.Add(new Alert { AlertStyle = alertStyle, Message = message, Dismissable = dismissable }); TempData[Alert.TempDataKey] = alerts; } }
public ActionResult Index() { var userInfo = new { Name = "Sirwan", LastName = "Afifi", }; ViewData["User"] = userInfo; ViewBag.User = userInfo; TempData["User"] = userInfo; return RedirectToAction("About"); }
@{ ViewBag.Title = "About"; } <h1>Tempdata</h1><p>@TempData["User"]</p> <h1>ViewData</h1><p>@ViewData["User"]</p> <h1>ViewBag</h1><p>@ViewBag.User</p>
public class NewsController : BaseController { readonly INewsService _newsService; readonly IUnitOfWork _uow; public NewsController(INewsService newsService, IUnitOfWork uow) { _newsService = newsService; _uow = uow; } [HttpPost] [ValidateAntiForgeryToken] [ValidateInput(false)] public ActionResult Create(News news) { if (ModelState.IsValid) { _newsService.AddNews(news); _uow.SaveChanges(); Success(string.Format("خبر با عنوان <b>{0}</b> با موفقیت ذخیره گردید!", news.Title), true); return RedirectToAction("Index"); } Danger("خطا در هنگام ثبت اطلاعات "); return View(news); } [HttpPost] public ActionResult Delete(int id) { _newsService.DeleteNewsById(id); _uow.SaveChanges(); Danger("اطلاعات مورد نظر با موفقیت حذف گردید!", true); return RedirectToAction("Index"); } }
@{ var alerts = TempData.ContainsKey(Alert.TempDataKey) ? (List<Alert>)TempData[Alert.TempDataKey] : new List<Alert>(); if (alerts.Any()) { <hr /> } foreach (var alert in alerts) { var dismissableClass = alert.Dismissable ? "alert-dismissable" : null; <div class="alert alert-@alert.AlertStyle @dismissableClass"> @if (alert.Dismissable) { <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> } @Html.Raw(alert.Message) </div> } }
<div> @{ Html.RenderPartial("_Alerts"); } @RenderBody() </div>