در نکتهی قبلی، در مورد JavaScript Isolation بحث شد؛ یک چنین قابلیتی در مورد فایلهای css. هم وجود دارد و جزو تازههای Blazor 5x است. در این حالت (CSS Isolation) میتوان شیوهنامهای را تهیه کرد که تنها مختص به یک کامپوننت است و بر روی سایر کامپوننتها تاثیری ندارد. این قابلیت نیاز به کدنویسی خاصی نداشته و به سادگی قابل استفادهاست: در این حالت اگر کامپوننتی برای مثال Counter.razor نام دارد، برای ایجاد فایل css مختص به آن، تنها کافی است در کنار فایل آن، فایل Counter.razor.css را ایجاد و تکمیل کرد. به این ترتیب هر شیوهنامهای که در این فایل تنظیم شود، فقط بر روی کامپوننت Counter تاثیرگذار خواهد بود؛ حتی اگر از این کامپوننت در کامپوننتهای دیگر نیز استفاده شود.
h1 { color:red; }
همانطور که مشاهده میکنید، در زمان build برنامه، تمام این شیوهنامههای اختصاصی کامپوننتهای مختلف برنامه، تبدیل به یک فایل خواهند شد (با نام project_name.styles.css) که توسط ویژگیهای HTML ای که به صورت خودکار تولید میشوند، از یکدیگر متمایز خواهند شد.
اگر علاقمند باشیم تا این شیوهنامه به فرزندان کامپوننت Counter نیز به ارث برسد، باید از روش زیر استفاده کرد (استفاده از deep combinator):
::deep h1 { color:red; }
مثال ساده آن کاربر و نقشهای او است. یک کاربر میتواند چندین نقش داشته باشد (نویسنده، ادیتور و غیره). یک نقش میتواند به چندین کاربر منتسب شود (مثلا نقش ادیتور را میشود به دهها کاربر انتساب داد). یعنی میشود از هر طرف این رابطه، یک رکورد را به چندین رکورد طرف دیگر ربط منطقی داد. اما در حالت مداخل یک آزمایش و مقادیر مجاز جهت یک مدخل، اینچنین نیست و رابطه one-to-many است.
مدیریت سفارشی سطوح دسترسی کاربران در MVC
- تنظیمات پیش فرض باید تغییر کنند تا کلمات عبور حداقل 10 کاراکتر باشند
- کلمه عبور حداقل یک عدد و یک کاراکتر ویژه باید داشته باشد
- امکان استفاده از 5 کلمه عبور اخیری که ثبت شده وجود ندارد
ایجاد اپلیکیشن جدید
در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.
بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک میکند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.
- کنترلر Account از کلاس UserManager استفاده میکند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده میکند که برای دسترسی و مدیریت دادههای کاربران استفاده میشود. در مثال ما این کلاس از Entity Framework استفاده میکند که پیاده سازی پیش فرض است.
- متد Register POST یک کاربر جدید میسازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی میکند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName }; var result = await UserManager.CreateAsync(user, model.Password); if (result.Succeeded) { await SignInAsync(user, isPersistent: false); return RedirectToAction("Index", "Home"); }
قانون 1: کلمههای عبور باید حداقل 10 کاراکتر باشند
- مقدار حداقل کاراکترهای کلمه عبور به دو شکل میتواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی میشود، همینجا میتوانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری میکند. سپس میتوان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext())) { PasswordValidator = new MinimumLengthValidator (10); } }
- حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
public AccountController() : this(new ApplicationUserManager()) { } public AccountController(ApplicationUserManager userManager) { UserManager = userManager; } public ApplicationUserManager UserManager { get; private set; }
- اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت میکنید.
قانون 2: کلمههای عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند
- در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده میکنیم.
public class CustomPasswordValidator : IIdentityValidator<string> { public int RequiredLength { get; set; } public CustomPasswordValidator(int length) { RequiredLength = length; } public Task<IdentityResult> ValidateAsync(string item) { if (String.IsNullOrEmpty(item) || item.Length < RequiredLength) { return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength))); } string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$"; if (!Regex.IsMatch(item, pattern)) { return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character")); } return Task.FromResult(IdentityResult.Success); }
- قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } }
قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد
public class PreviousPassword { public PreviousPassword() { CreateDate = DateTimeOffset.Now; } [Key, Column(Order = 0)] public string PasswordHash { get; set; } public DateTimeOffset CreateDate { get; set; } [Key, Column(Order = 1)] public string UserId { get; set; } public virtual ApplicationUser User { get; set; } }
- خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser { public ApplicationUser() : base() { PreviousUserPasswords = new List<PreviousPassword>(); } public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; } }
public class ApplicationUserStore : UserStore<ApplicationUser> { public ApplicationUserStore(DbContext context) : base(context) { } public override async Task CreateAsync(ApplicationUser user) { await base.CreateAsync(user); await AddToPreviousPasswordsAsync(user, user.PasswordHash); } public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password) { user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password }); return UpdateAsync(user); } }
public class ApplicationUserManager : UserManager<ApplicationUser> { private const int PASSWORD_HISTORY_LIMIT = 5; public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext())) { PasswordValidator = new CustomPasswordValidator(10); } public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword) { if (await IsPreviousPassword(userId, newPassword)) { return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password")); } var result = await base.ResetPasswordAsync(userId, token, newPassword); if (result.Succeeded) { var store = Store as ApplicationUserStore; await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword)); } return result; } private async Task<bool> IsPreviousPassword(string userId, string newPassword) { var user = await FindByIdAsync(userId); if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate). Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT) .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any()) { return true; } return false; } }
سورس کد این مثال را میتوانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.
Blazor به صورت توکار به همراه تعدادی کنترل ورودی مانند InputText، InputTextArea، InputSelect، InputNumber، InputCheckbox و InputDate است که با سیستم اعتبارسنجی ورودیهای آن نیز یکپارچه هستند.
در یک برنامهی واقعی نیاز است divهایی مانند زیر را که به همراه روش تعریف این کامپوننتهای ورودی است، صدها بار در قسمتهای مختلف تکرار کرد:
<EditForm Model="NewPerson" OnValidSubmit="HandleValidSubmit"> <DataAnnotationsValidator /> <div class="form-group"> <label for="firstname">First Name</label> <InputText @bind-Value="NewPerson.FirstName" class="form-control" id="firstname" /> <ValidationMessage For="NewPerson.FirstName" /> </div>
تمام کامپوننتهای ورودی Blazor از کلاس پایهی ویژهای به نام <InputBase<T مشتق شدهاند. این کلاس است که کار یکپارچگی با EditContext را جهت ارائهی اعتبارسنجیهای لازم، انجام میدهد. همچنین کار binding را نیز با ارائهی پارامتر Value از نوع T انجام میدهد که نوشتن یک چنین کدهایی مانند "bind-Value="myForm.MyValue@ را میسر میکند. InputBase یک کلاس جنریک است که خاصیت Value آن از نوع T است. از آنجائیکه مرورگرها اطلاعات را به صورت رشتهای در اختیار ما قرار میدهند، این کامپوننت نیاز به روشی را دارد تا بتواند ورودی دریافتی را به نوع T تبدیل کند و اینکار را میتوان با بازنویسی متد TryParseValueFromString آن انجام داد:
protected abstract bool TryParseValueFromString(string value, out T result, out string validationErrorMessage);
یک مثال: کامپوننت جدید Shared\InputPassword.razor
@inherits InputBase<string> <input type="password" @bind="@CurrentValue" class="@CssClass" /> @code { protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage) { result = value; validationErrorMessage = null; return true; } }
<EditForm Model="userInfo" OnValidSubmit="CreateUser"> <DataAnnotationsValidator /> <InputPassword class="form-control" @bind-Value="@userInfo.Password" />
- در این مثال CurrentValue و CssClass از کلاس پایهی InputBase تامین میشوند.
- هربار که مقدار ورودی وارد شدهی توسط کاربر تغییر کند، متد TryParseValueFromString اجرا میشود.
- در متد TryParseValueFromString، مقدار validationErrorMessage به نال تنظیم شده؛ یعنی اعتبارسنجی خاصی مدنظر نیست. اولین پارامتر آن مقداری است که از کاربر دریافت شده و دومین پارامتر آن مقداری است که به کامپوننت ورودی که از آن ارثبری کردهایم، ارسال میشود تا CurrentValue را تشکیل دهد (و یا خاصیت CurrentValueAsString نیز برای این منظور وجود دارد).
- اگر اعتبارسنجی اطلاعات ورودی در متد TryParseValueFromString با شکست مواجه شود، مقدار false را باید بازگشت داد.
- وقتی دیتایی داریم که به تکرار از آن در برنامه استفاده میکنیم.
- وقتی بعد از گرفتن دیتایی از دیتابیس، محاسباتی بر روی آن انجام میدهیم و پاسخ نهایی محاسبه را به کاربر نمایش میدهیم، میتوانیم یکبار پاسخ را کش کنیم تا از محاسبهی هر بارهی آن جلوگیری شود.
- سخت افزاری که برای کش استفاده میکنیم یعنی Ram، بسیار گرانتر از دیتابیس برای ما تمام میشود؛ چرا که محدود است.
- اگر همه دیتاهارا کش کنید، عمل سرچ میان آن زمان بیشتری خواهد برد.
این روش تا زمانیکه برنامهی ما برای اجرا شدن، تنها از یک سرور استفاده کند، بهترین انتخاب خواهد بود؛ چرا که به دلیل نزدیک بودن، سریعترین بازخورد را نیز به درخواستها ارائه میدهد.
اما شرایطی را فرض کنید که برنامه از چندین سرور برای اجرا شدن استفاده میکند و به طبع هر سرور درخواستهای خودش را داراست که ما باید برای هر یک بصورت جداگانهای یک کش In-Memory را در حافظه Ram هرکدام ایجاد کنیم.
فرض کنید دیتای ما 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 باشد. بخشی از دیتا در Server 1 کش میشود (1 , 3 , 5 , 9) و بخشی دیگر در Server 2 کش خواهد شد (2 , 4 , 6 ,7 , 8 , 10).
در اینجا مشکلات و ضعف هایی به وجود خواهد آمد :
- برای مثال اگر Server 1 به هر دلیلی از بین برود یا Down شود، اطلاعات کش درون آن نیز پاک خواهد شد و بعد از راه اندازی باید همه آن را دوباره از دیتابیس بخواند.
- هر کدام از سرورها کشهای جدایی دارند و باهم Sync نیستند و امکان وجود یک دادهی حیاتی در یکی و عدم وجود آن در دیگری، بالاست. فرض کنید برنامه برای هر درخواست، نیاز به اطلاعات دسترسی کاربری را دارد. دسترسیهای کاربر، در Server 1 کش شده، اما در Server 2 موجود نیست. در Server 2 به دلیل عدم وجود این کش، برنامه برای درخواستهای معمول خود و چک کردن دسترسی کاربر یا باید هربار به دیتابیس درخواستی را ارسال کند که این برخلاف خواسته ماست و یا باید دیتای مربوط به دسترسیهای کاربر را بعد از یکبار درخواست، از دیتابیس در خودش کش کند که اینهم دوباره کاری به حساب میاید و دوبار کش کردن یک دیتا، امر مطلوبی نخواهد بود.
روش هایی وجود دارد که بتوان از سیستم Local Caching در حالت چند سروری هم استفاده کرد و این مشکلات را از بین برد، اما روش استاندارد در حالت چند سروری، استفاده از Distributed Cacheها است.
روش دوم : Distributed Caching
در این روش برنامهی ما برای اجرا شدن از چندین سرور شبکه شده به هم، در حال استفاده هست و Cache برنامه، توسط سرورها به اشتراک گذاشته شده.
در این حالت سرورهای ما از یک کش عمومی استفاده میکنند که مزایای آن شامل :
■ درخواستها به چندین سرور مختلف از هم ارسال شده، اما دیتای کش بصورت منسجم در هریک وجود خواهد داشت.
■ با خراب شدن یا Down شدن یک سرور، کش موجود در سرورهای دیگر پاک نمیشود و کماکان قابل استفاده است.
■ به حافظه Ram یک سرور محدود نیست و مشکلات زیادی همچون کمبود سخت افزاری و محدودیتهای حافظهی Ram را تا حد معقولی کاهش میدهد.
طریقه استفاده از Cache در Asp.Net Core :
- بر خلاف ASP.NET web forms و ASP.NET MVC در نسخههای Core به بعد، Cache بصورت از پیش ثبت شده، وجود ندارد. کش در Asp.Net Core با فراخوانی سرویسهای مربوطهی آن قابل استفاده است و نیاز است قبل از استفاده، سرویس آن را در کلاس Startup برنامه فراخوانی کنید.
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddMemoryCache(); }
- اینترفیس IMemoryCache از سیستم تزریق وابستگیها در Core استفاده میکند و برای استفاده از اینترفیس آن، پس از اضافه کردن MemoryCache به Startup ، باید در کنترلر، عمل تزریق وابستگی (DI) را انجام دهید؛ سپس متدهای مورد نیاز برای کش، در دسترس خواهد بود.
public class HomeController : Controller { private readonly IMemoryCache _cache; public HomeController(IMemoryCache cache) { _cache = cache; } .... }
- برای ذخیرهی کش میتوانید از متد Set موجود در این اینترفیس استفاده کنید.
public IActionResult Set() { _cache.Set("CacheKey", data , TimeSpan.FromDays(1)); return View(); }
در پارامتر اول این متد (CacheKey)، یک کلید، برای اطلاعاتی که میخواهیم کش کنیم قرار میدهیم. دقت کنید که این کلید، شناسهی دیتای شماست و باید طوری آن را در نظر گرفت که با صدا زدن این کلید از سرویس کش، همان دیتای مورد نظر را برگشت دهد (هر Object دیتا، باید کلید Unique خود را داشته باشد).
در پارامتر دوم، دیتای مورد نظر را که میخواهیم کش کنیم، به متد میدهیم و در پارامتر سوم نیز زمان اعتبار و تاریخ انقضای دیتای کش شده را وارد میکنیم؛ به این معنا که دیتای کش شده، بعد از مدت زمان گفته شده، از حافظه کش(Ram) حذف شود و برای دسترسی دوباره و کش کردن دوباره اطلاعات، نیاز به خواندن مجدد از دیتابیس باشد.
- برای دسترسی به اطلاعات کش شده میتوانید از متد Get استفاده کنید.
public IActionResult Get() { string data = _cache.Get("CacheKey"); return View(data); }
تنها پارامتر ورودی این متد، کلید از قبل نسبت داده شده به اطلاعات کش هست که با استفاده از یکسان بودن کلید در ورودی این متد و کلید Set شده از قبل در حافظه Ram، دیتا مربوط به آن را برگشت میدهد.
- متد TryGetValue برای بررسی وجود یا عدم وجود یک کلید در حافظه کش هست و یک Boolean را خروجی میدهد.
public IActionResult Set() { DateTime data; // Look for cache key. if (!_cache.TryGetValue( "CacheKey" , out data)) { // Key not in cache, so get data. data= DateTime.Now; // Save data in cache and set the relative expiration time to one day _cache.Set( "CacheKey" , data, TimeSpan.FromDays(1)); } return View(data); }
این متد ابتدا بررسی میکند که کلیدی با نام "CacheKey" وجود دارد یا خیر؟ در صورت عدم وجود، آن را میسازد و دیتای مورد نظر را به آن نسبت میدهد.
- با استفاده از متد GetOrCreate میتوانید کار متدهای Get و Set را باهم انجام دهید و در یک متد، وجود یا عدم وجود کش را بررسی و در صورت وجود، مقداری را return و در صورت عدم وجود، ابتدا ایجاد کش و بعد return مقدار کش شده را انجام دهید.
public IActionResult GetOrCreate() { var data = _cache.GetOrCreate( "CacheKey" , entry => entry.SlidingExpiration = TimeSpan.FromSeconds(3); return View(data); }); return View(data); }
- برای مدیریت حافظهی Ram شما باید یک Expiration Time را برای کشهای خود مشخص کنید؛ تا هم حافظه Ram را حجیم نکنید و هم در هر بازهی زمانی، دیتای بروز را از دیتابیس بخوانید. برای این کار optionهای متفاوتی از جمله absolute expiration و sliding expiration وجود دارند.
در اینجا absolute expiration به این معنا است که یک زمان قطعی را برای منقضی شدن کشها مشخص میکند؛ به عبارتی میگوییم کش با کلید فلان، در تاریخ و ساعت فلان حذف شود. اما در sliding expiration یک بازه زمانی برای منقضی شدن کشها مشخص میکنیم؛ یعنی میگوییم بعد از گذشت فلان دقیقه از ایجاد کش، آن را حذف کن و اگر در طی این مدت مجددا خوانده شد، طول مدت زمان آن تمدید خواهد شد.
این تنظیمات را میتوانید در قالب یک option زمان Set کردن یک کش، به آن بدهید.
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions(); options.AbsoluteExpiration = DateTime.Now.AddMinutes(1); options.SlidingExpiration = TimeSpan.FromMinutes(1); _cache.Set("CacheKey", data, options );
در مثال بالا هردو option اضافه شده یک کار را انجام میدهند؛ با این تفاوت که absolute expiration تاریخ now را گرفته و یک دقیقه بعد را به آن اضافه کرده و تاریخ انقضای کش را با آن تاریخ set میکند. اما sliding expiration از حالا بمدت یک دقیقه اعتبار دارد.
- یکی از روشهای مدیریت حافظه Ram در کشها این است که برای حذف شدن کشها از حافظه، اولویت بندیهایی را تعریف کنید. اولویتها در چهار سطح قابل دسترسی است:
- NeverRemove = 3
- High = 2
- Normal = 1
- Low = 0
این اولویت بندیها زمانی کاربرد خواهند داشت که حافظه اختصاصی Ram، برای کشها پر شده باشد و در این حالت سیستم کشینگ بصورت خودمختار، کشهای با الویت پایین را از حافظه حذف میکند و کشهای با الویت بیشتر، در حافظه باقی میمانند. این با شماست که الویت را برای دیتاهای خود تعیین کنید؛ پس باید با دقت و فکر شده این کار را انجام دهید.
MemoryCacheEntryOptions options = new MemoryCacheEntryOptions(); // Low / Normal / High / NeverRemove options.Priority = CacheItemPriority.High; cache.Set("CacheKey", data, options);
به این صورت میتوانید الویتهای متفاوت را در قالب option به کشهای خود اختصاص دهید.
در این مقاله سعی شد مفاهیم اولیه Cache، طوری گفته شود، تا برای افرادی که میخواهند به تازگی این سیستم را بیاموزند و در پروژههای خود استفاده کنند، کاربردی باشد و درک نسبی را نسبت به مزایا و محدودیتهای این سیستم بدست آورند.
در قسمت دوم همین مقاله بطور تخصصیتر به این مبحث میپردازیم و یک پکیج آماده را معرفی میکنیم که خیلی راحتتر و اصولیتر کش را برای ما پیاده سازی میکند.
روش متداول تعریف فونت در iTextSharp به صورت زیر است:
public static iTextSharp.text.Font Tahoma()
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
var baseFont = BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
return new Font(baseFont);
}
از آنجائیکه خصوصا برای متون فارسی نیاز است تا به ازای هر المان کوچکی این فونت تنظیم شود و در غیر اینصورت متنی نمایش داده نخواهد شد، با سربار بالایی مواجه خواهیم شد. بنابراین به نظر میرسد که بهتر باشد این تولید اشیاء فونت را کش کنیم. خوشبختانه iTextSharp سیستم کش کردن تعریف قلمهای متفاوت را هم به صورت توکار دارا است:
public static iTextSharp.text.Font GetTahoma()
{
var fontName = "Tahoma";
if (!FontFactory.IsRegistered(fontName))
{
var fontPath = Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf";
FontFactory.Register(fontPath);
}
return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
}
کلاس FontFactory کار ثبت و بازیابی قلمهای متفاوت را به عهده دارد. تنها کافی است یکبار قلمی در آن ثبت شود (FontFactory.Register)، بار دیگر اطلاعات قلم به سادگی از کش FontFactory خوانده خواهد شد (FontFactory.GetFont).
استفاده از ادیتور CKEditor در صفحات ASP.NET
- در MVC میتونید فقط به ازای یک خاصیت، AllowHtml رو فعال کنید و نه به اجبار به ازای کل صفحه یا کل سایت (مثل وب فرمها).
- بعد از اینکه AllowHtml رو فعال کردید، نیاز خواهید داشت ورودی کاربر را اندکی تمیز کنید.