http://www.pageglimpse.com/features/api
http://www.pageglimpse.com/features/api
@typeparam T @inherits InputBase<T>
protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { // ... } protected override string FormatValueAsString(T? value) { // ... }
<link href="_content/BlazorPersianJavaScriptDatePicker.Lib/css/PersianDatePicker.css" rel="stylesheet"/> <script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/PersianDatePicker.js?v=1"></script>
@using BlazorPersianJavaScriptDatePicker.Lib
@typeparam T @inherits InputBase<T> <div> <span style="cursor:pointer" onclick="PersianDatePicker.Show(document.getElementById('@ElementId'), '@Today')"> 📅 </span> <input @attributes="@AdditionalAttributes" type="text" dir="ltr" @ref="ElementReference" name="@ElementId" id="@ElementId" autocapitalize="off" autocorrect="off" autocomplete="off" value="@EnteredValue" @oninput="OnInput"/> @if (ValueExpression is not null) { <ValidationMessage For="@ValueExpression"/> } </div>
function setValue(date) { _textBox.value = date; // NOTE: To notify the addEventListener('change', fn) _textBox.dispatchEvent(new Event('change')); _textBox.focus(); hide(); try { _textBox.onchange(); }catch(ex) {} }
window.activateDatePicker = { enableDatePicker: function (element, objectReference) { element.addEventListener('change', function (evt) { objectReference.invokeMethodAsync("OnInputFieldChanged", this.value); }); } };
<script src="_content/BlazorPersianJavaScriptDatePicker.Lib/js/activateDatePicker.js?v=1"></script>
public partial class PersianDatePicker<T> : IDisposable { private bool _isDisposed; private DotNetObjectReference<PersianDatePicker<T>>? _objectReference; private string ElementId { get; } = Guid.NewGuid().ToString("N"); private ElementReference? ElementReference { set; get; } private string Today { get; } = DateTime.Now.ToShortPersianDateString(); [Inject] private IJSRuntime JsRuntime { set; get; } = default!; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { _objectReference = DotNetObjectReference.Create(this); await JsRuntime.InvokeVoidAsync("activateDatePicker.enableDatePicker", ElementReference, _objectReference); EnteredValue = CurrentValueAsString; StateHasChanged(); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (!_isDisposed) { try { _objectReference?.Dispose(); } finally { _isDisposed = true; } } } }
[JSInvokable] public void OnInputFieldChanged(string? value)
public partial class PersianDatePicker<T> : IDisposable { private string? _enteredValue; private string? EnteredValue { set => _enteredValue = value; get => UsePersianNumbers ? _enteredValue.ToPersianNumbers() : _enteredValue; } [Parameter] public bool UsePersianNumbers { set; get; } [Parameter] public string ParsingErrorMessage { get; set; } = "لطفا در ورودی {0} تاریخ شمسی معتبری را وارد نمائید."; [Parameter] public int BeginningOfCentury { set; get; } = 1400; private void OnInput(ChangeEventArgs e) { SetCurrentValue(e.Value as string); } private void SetCurrentValue(string? value) { EnteredValue = value; CurrentValueAsString = value; } [JSInvokable] public void OnInputFieldChanged(string? value) { SetCurrentValue(value); } protected override void OnInitialized() { base.OnInitialized(); SanityCheck(); } protected override bool TryParseValueFromString( string? value, [MaybeNullWhen(false)] out T result, [NotNullWhen(false)] out string? validationErrorMessage) { validationErrorMessage = string.Format(CultureInfo.InvariantCulture, ParsingErrorMessage, DisplayName); if (!value.TryParsePersianDateToDateTimeOrDateTimeOffset(out result, BeginningOfCentury)) { return false; } if (result is null) { throw new InvalidOperationException(validationErrorMessage); } validationErrorMessage = null; return true; } protected override string FormatValueAsString(T? value) { return !string.IsNullOrWhiteSpace(EnteredValue) ? EnteredValue : value.FormatDateToShortPersianDate(); } private void SanityCheck() { if (!Value.IsDateTimeOrDateTimeOffsetType()) { throw new InvalidOperationException( "The `Value` type is not a supported `date` type. DateTime, DateTime?, DateTimeOffset and DateTimeOffset? are supported."); } } // ... }
using System.ComponentModel.DataAnnotations; namespace BlazorPersianJavaScriptDatePicker.ViewModels; public class InputPersianDateViewModel { [Required] public string Name { set; get; } = default!; [Required] public DateTime BirthDayGregorian { set; get; } = DateTime.Now.AddYears(-40); public DateTime? LoginAt { set; get; } = DateTime.Now.AddMinutes(-2); [Required] public DateTimeOffset LogoutAt { set; get; } public DateTimeOffset? RegisterAt { set; get; } = DateTimeOffset.Now.AddMinutes(-10); }
<EditForm Model="Model" OnValidSubmit="DoSave"> <DataAnnotationsValidator/> <div> <label>تاریخ تولد</label> <div> <PersianDatePicker @bind-Value="Model.BirthDayGregorian" UsePersianNumbers="false" /> </div> </div> <button type="submit">ارسال</button> </EditForm>
<?xml version="1.0" encoding="utf-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>Url Page 1</loc> <lastmod>2014-02-20</lastmod> <changefreq>always</changefreq> <priority>1</priority> </url> <url> <loc>Url Page 2</loc> <lastmod>2014-02-20</lastmod> <changefreq>always</changefreq> <priority>1</priority> </url> </urlset>
public void UpdateSiteMap(string Addr,string NewOpr)
if (NewOpr != "add") { XmlElement xmlElement = xmlDoc.DocumentElement; if (xmlElement.ChildNodes != null) { foreach (XmlElement myElement in xmlDoc.DocumentElement) { if (myElement.ChildNodes[0].InnerText == Addr) { if (NewOpr != "delete") myElement.ChildNodes[1].InnerText = DateTime.Now.ToString("yyyy-MM-dd"); else myElement.ParentNode.RemoveChild(myElement); break; } } } }
else { string ns="http://www.sitemaps.org/schemas/sitemap/0.9"; XmlNode url = xmlDoc.CreateNode(XmlNodeType.Element, "url",ns ); XmlNode loc = xmlDoc.CreateNode(XmlNodeType.Element, "loc", ns); XmlNode lastmod = xmlDoc.CreateNode(XmlNodeType.Element, "lastmod", ns); XmlNode changefreq = xmlDoc.CreateNode(XmlNodeType.Element, "changefreq", ns); XmlNode priority = xmlDoc.CreateNode(XmlNodeType.Element, "priority", ns); loc.InnerText = Addr; lastmod.InnerText = DateTime.Now.ToString("yyyy-MM-dd"); changefreq.InnerText = "always"; priority.InnerText = "1"; url.AppendChild(loc); url.AppendChild(lastmod); url.AppendChild(changefreq); url.AppendChild(priority); xmlDoc.DocumentElement.AppendChild(url); }
// Add Info In Table // updateSiteMap updateSiteMap = new updateSiteMap(); updateSiteMap.UpdateSiteMap("Url Page", "add");
updateSiteMap updateSiteMap = new updateSiteMap(); updateSiteMap.UpdateSiteMap("Url Page", "edit");
updateSiteMap updateSiteMap = new updateSiteMap(); updateSiteMap.UpdateSiteMap("Url Page", "delete");
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider