مطالب
شروع به کار با DNTFrameworkCore - قسمت 7 - ارتقاء به نسخه ‭4.5.x
بعد از انتشار قسمت 6 به عنوان آخرین قسمت مرتبط با تفکر مبتنی‌بر CRUD‏ ‎(‎CRUD-based thinking)‎ قصد دارم پشتیبانی از طراحی Application Layer مبتنی‌بر CQRS را نیز به این زیرساخت اضافه کنم.
در این مطلب تغییرات حاصل از طراحی مجدد و بازسازی انجام شده در نسخه جدید را مرور خواهیم کرد.

تغییرات کتابخانه DNTFrameworkCore

1- واسط‌های مورد استفاده جهت ردیابی موجودیت‌ها :
public interface ICreationTracking
{
    DateTime CreatedDateTime { get; set; }
}

public interface IModificationTracking
{
    DateTime? ModifiedDateTime { get; set; }
}
علاوه بر تغییر نام و نوع داده خصوصیت‌های تاریخ ایجاد و ویرایش، سایر خصوصیات به صورت خواص سایه‌ای در کتابخانه DNTFrameworkCore.EFCore مدیریت خواهند شد. 
2. با اضافه شدن واسط IHasRowIntegrity برای پشتیبانی از امکان تشخیص اصالت ردیف‌های یک بانک اطلاعاتی با استفاده از EF Core، خصوصیت RowVersion به Version تغییر نام پیدا کرد.
public interface IHasRowIntegrity
{
    string Hash { get; set; }
}

public interface IHasRowVersion
{
    byte[] Version { get; set; }
}
3- ارث‌بری از کلاس AggregateRoot در سناریوهای CRUD و در زمان استفاده از CrudService هیچ ضرورتی ندارد و صرفا برای پشتیبانی از طراحی مبتنی‌بر DDD کاربرد خواهد داشت. اگر قصد طراحی یک Rich Domain Model را دارید و رویکرد DDD را دنبال می‌کنید، با استفاده از کلاس پایه AggregateRoot امکان مدیریت DomainEventهای مرتبط با یک Aggregate را خواهید داشت. 
public abstract class AggregateRoot<TKey> : Entity<TKey>, IAggregateRoot
    where TKey : IEquatable<TKey>
{
    private readonly List<IDomainEvent> _events = new List<IDomainEvent>();
    public IReadOnlyCollection<IDomainEvent> Events => _events.AsReadOnly();

    protected virtual void AddDomainEvent(IDomainEvent newEvent)
    {
        _events.Add(newEvent);
    }

    public virtual void ClearEvents()
    {
        _events.Clear();
    }
}
4- امکان Publish رخ‌دادهای مرتبط با یک AggregateRoot به IEventBus اضافه شده است:
public static class EventBusExtensions
{
    public static Task TriggerAsync(this IEventBus bus, IEnumerable<IDomainEvent> events)
    {
        var tasks = events.Select(async domainEvent => await bus.TriggerAsync(domainEvent));
        return Task.WhenAll(tasks);
    }

    public static async Task PublishAsync(this IEventBus bus, IAggregateRoot aggregateRoot)
    {
        await bus.TriggerAsync(aggregateRoot.Events);
        aggregateRoot.ClearEvents();
    }
}
5- واسط IDbSeed به IDbSetup تغییر نام پیدا کرده است.

6- اضافه شدن یک سرویس برای ذخیره‌سازی اطلاعات به صورت Key/Value در بانک اطلاعاتی:
public interface IKeyValueService : IApplicationService
{
    Task SetValueAsync(string key, string value);
    Task<Maybe<string>> LoadValueAsync(string key);
    Task<bool> IsTamperedAsync(string key);
}

public class KeyValue : Entity, IModificationTracking, ICreationTracking, IHasRowIntegrity
{
    public string Key { get; set; }
    [Encrypted] public string Value { get; set; }
    public string Hash { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public DateTime? ModifiedDateTime { get; set; }
}
7- AuthorizationProvider حذف شده و جمع آوری دسترسی‌های سیستم به عهده خود استفاده کننده از این زیرساخت می‌باشد.

8- اضافه شدن امکان Exception Mapping و همچنین سفارشی سازی پیغام‌های خطای عمومی:
    public class ExceptionOptions
    {
        public List<ExceptionMapItem> Mappings { get; } = new List<ExceptionMapItem>();

        [Required] public string DbException { get; set; }
        [Required] public string DbConcurrencyException { get; set; }
        [Required] public string InternalServerIssue { get; set; }

        public bool TryFindMapping(DbException dbException, out ExceptionMapItem mapping)
        {
            mapping = null;

            var words = new HashSet<string>(Regex.Split(dbException.ToStringFormat(), @"\W"));

            var mappingItem = Mappings.FirstOrDefault(a => a.Keywords.IsProperSubsetOf(words));
            if (mappingItem == null)
            {
                return false;
            }

            mapping = mappingItem;

            return true;
        }
    }
و روش استفاده از آن را در پروژه DNTFrameworkCore.TestAPI می‌توانید مشاهده کنید. برای معرفی نگاشت‌ها، می‌توان به شکل زیر در فایل appsetting.json عمل کرد:
"Exception": {
  "Mappings": [
    {
      "Message": "به دلیل وجود اطلاعات وابسته امکان حذف وجود ندارد",
      "Keywords": [
        "DELETE",
        "REFERENCE"
      ]
    },
    {
      "Message": "یک تسک با این عنوان قبلا در سیستم ثبت شده است",
      "MemberName": "Title",
      "Keywords": [
        "Task",
        "UIX_Task_NormalizedTitle"
      ]
    }
  ],
  "DbException": "امکان ذخیره‌سازی اطلاعات وجود ندارد؛ دوباره تلاش نمائید",
  "DbConcurrencyException": "اطلاعات توسط کاربری دیگر در شبکه تغییر کرده است",
  "InternalServerIssue": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!"
}

8- اطلاعات مرتبط با مستأجر جاری در سناریوهای چند مستأجری از واسط IUserSession حذف شده و به واسط ITenantSession منتقل شده است. نوع داده خصوصیت UserId به String تغییر پیدا کرده و بر اساس نیاز می‌توان به شکل زیر از آن استفاده کرد:
_session.UserId
_session.UserId<long>()
_session.UserId<int>()
_session.UserId<Guid>()

علاوه بر آن خصوصیت ImpersonatorUserId که می‌تواند حاوی UserId کاربری باشد که در نقش کاربر دیگری در سناریوهای Impersonation وارد سیستم شده است؛ این مورد در سیستم Logging مبتنی‌بر فایل سیستم و بانک اطلاعاتی موجود در این زیرساخت، ثبت و نگهداری می‌شود.
9- لیست ClaimTypeهای مورد استفاده در این زیرساخت:
public static class UserClaimTypes
{
    public const string UserName = ClaimTypes.Name;
    public const string UserId = ClaimTypes.NameIdentifier;
    public const string SerialNumber = ClaimTypes.SerialNumber;
    public const string Role = ClaimTypes.Role;
    public const string DisplayName = nameof(DisplayName);
    public const string BranchId = nameof(BranchId);
    public const string BranchName = nameof(BranchName);
    public const string IsHeadOffice = nameof(IsHeadOffice);
    public const string TenantId = nameof(TenantId);
    public const string TenantName = nameof(TenantName);
    public const string IsHeadTenant = nameof(IsHeadTenant);
    public const string Permission = nameof(Permission);
    public const string PackedPermission = nameof(PackedPermission);
    public const string ImpersonatorUserId = nameof(ImpersonatorUserId);
    public const string ImpersonatorTenantId = nameof(ImpersonatorTenantId);
}
از خصوصیات Branch*‎ برای سناریوهای چند شعبه‎‌ای می‌توان استفاده کرد که در این صورت اگر یکی از شعب به عنوان دفتر مرکزی در نظر گرفته شود باید Claim‌ای با نام IsHeadOffice با مقدار true از زمان ورود به سیستم برای کاربران آن شعبه در نظر گرفته شود. 
خصوصیات Tenant*‎ برای سناریوهای چند مستأجری در نظر گرفته شده است که اگرطراحی مورد نظرتان به نحوی باشد که بخش مدیریت مستأجرهای سیستم در همان سیستم پیاده‌سازی شده باشد یا به تعبیری سیستم Host و Tenant یکی باشند، می‌توان Claim‌ای با نام IsHeadTenant با مقدار true در زمان ورود به سیستم برای کاربران Host (مستأجر اصلی) در نظر گرفته شود.
‌‌
10- مکانیزم Logging مبتنی‌بر فایل سیستم:
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder)
{
    builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
    return builder;
}


/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure)
{
    builder.AddFile();
    builder.Services.Configure(configure);

    return builder;
}
11- امکان TenantResolution برای شناسایی مستأجر جاری سیستم:
public interface ITenantResolutionStrategy
{
    string TenantId();
}

public interface ITenantStore
{
    Task<Tenant> FindTenantAsync(string tenantId);
}
از این واسط‌ها در میان افزار TenantResolutionMiddleware موجود در کتابخانه DNTFrameworkCore.Web.Tenancy استفاده شده است. و همچنین جهت دسترسی به اطلاعات مستأجر جاری سیستم می‌توان واسط زیر را تزریق و استفاده کرد:
public interface ITenantSession : IScopedDependency
{
    /// <summary>
    ///     Gets current TenantId or null.
    ///     This TenantId should be the TenantId of the <see cref="IUserSession.UserId" />.
    ///     It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
    /// </summary>
    string TenantId { get; }

    /// <summary>
    ///     Gets current TenantName or null.
    ///     This TenantName should be the TenantName of the <see cref="IUserSession.UserId" />.
    ///     It can be null if given <see cref="IUserSession.UserId" /> is a head-tenant user or no user logged in.
    /// </summary>
    string TenantName { get; }

    /// <summary>
    ///     Represents current tenant is head-tenant.
    /// </summary>
    bool IsHeadTenant { get; }

    /// <summary>
    ///     TenantId of the impersonator.
    ///     This is filled if a user with <see cref="IUserSession.ImpersonatorUserId" /> performing actions behalf of the
    ///     <see cref="IUserSession.UserId" />.
    /// </summary>
    string ImpersonatorTenantId { get; }
}
12- استفاده از SystemTime و IClock برای افزایش تست‌پذیری سناریوهای درگیر با DateTime:
public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.UtcNow;

    public static Func<DateTime, DateTime> Normalize = (dateTime) =>
        DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
}
public interface IClock : ITransientDependency
{
    DateTime Now { get; }
    DateTime Normalize(DateTime dateTime);
}

internal sealed class Clock : IClock
{
    public DateTime Now => SystemTime.Now();

    public DateTime Normalize(DateTime dateTime)
    {
        return SystemTime.Normalize(dateTime);
    }
}
13- تغییر واسط عمومی کلاس Result:
public class Result
{
    private static readonly Result _ok = new Result(false, string.Empty);
    private readonly List<ValidationFailure> _failures;

    protected Result(bool failed, string message) : this(failed, message,
        Enumerable.Empty<ValidationFailure>())
    {
        Failed = failed;
        Message = message;
    }

    protected Result(bool failed, string message, IEnumerable<ValidationFailure> failures)
    {
        Failed = failed;
        Message = message;
        _failures = failures.ToList();
    }

    public bool Failed { get; }
    public string Message { get; }
    public IEnumerable<ValidationFailure> Failures => _failures.AsReadOnly();

    [DebuggerStepThrough]
    public static Result Ok() => _ok;

    [DebuggerStepThrough]
    public static Result Fail(string message)
    {
        return new Result(true, message);
    }

    //...
}

روش معرفی سرویس‌های مرتبط با کتابخانه DNTFrameworkCore
services.AddFramework()
    .WithModelValidation()
    .WithFluentValidation()
    .WithMemoryCache()
    .WithSecurityService()
    .WithBackgroundTaskQueue()
    .WithRandomNumber();
متد WithFluentValidation یک متد الحاقی برای FrameworkBuilder می‌باشد که در کتابخانه DNTFrameworkCore.FluentValidation تعریف شده است.

تغییرات کتابخانه DNTFrameworkCore.EFCore

1- اگر از CrudService پایه موجود استفاده می‌کنید، محدودیت ارث‌بری از TrackableEntity از موجودیت اصلی برداشته شده است. همچنین همانطور که در نظرات مطالب قبلی در قالب نکته تکمیلی اشاره شد، متد  MapToEntity  به نحوی تغییر کرد که پاسخگوی اکثر نیازها باشد.
2- امکان تنظیم ModifiedProperties  برای موجودیت‌های وابسته در سناریوهایی با موجودیت‌های وابسته Master-Detail نیز مهیا شده است.
public abstract class TrackableEntity<TKey> : Entity<TKey>, ITrackable where TKey : IEquatable<TKey>
{
    [NotMapped] public TrackingState TrackingState { get; set; }
    [NotMapped] public ICollection<string> ModifiedProperties { get; set; }
}
3-  امکان ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در یک بانک اطلاعاتی با استفاده از EF ، اضافه شده است که از همان موجودیت KeyValue برای نگهداری مقادیر استفاده می‌کند:
public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddEFCore(this IConfigurationBuilder builder,
        IServiceProvider provider)
    {
        return builder.Add(new EFConfigurationSource(provider));
    }
}
4- واسط IHookEngine حذف شده و سازنده کلاس پایه DbContextCore لیستی از IHook را به عنوان پارامتر می‌پذیرد:
protected DbContextCore(DbContextOptions options, IEnumerable<IHook> hooks) : base(options)
{
    _hooks = hooks ?? throw new ArgumentNullException(nameof(hooks));
}
 همچنین امکان IgnoreHook برای غیرفعال کردن یک Hook خاص با استفاده از نام آن مهیا شده است:
public void IgnoreHook(string hookName)
{
    _ignoredHookList.Add(hookName);
}
امکان پیاده سازی Hook سفارشی را برای سناریوهای خاص هم با پیاده سازی واسط IHook و یا با ارث‌بری از کلاس‌های پایه موجود در زیرساخت، خواهید داشت. به عنوان مثال:
internal sealed class RowIntegrityHook : PostActionHook<IHasRowIntegrity>
{
    public override string Name => HookNames.RowIntegrity;
    public override int Order => int.MaxValue;
    public override EntityState HookState => EntityState.Unchanged;

    protected override void Hook(IHasRowIntegrity entity, HookEntityMetadata metadata, IUnitOfWork uow)
    {
        metadata.Entry.Property(EFCore.Hash).CurrentValue = uow.EntityHash(entity);
    }
}
در بازطراحی انجام شده، دسترسی به وهله جاری DbContext هم از طریق واسط IUnitOfWork مهیا شده است.
5- متد EntityHash به واسط IUnitOfWork اضافه شده است که امکان محاسبه هش مرتبط با یک رکورد از یک موجودیت خاص را مهیا می‌کند؛ همچنین امکان تغییر الگوریتم و سفارشی سازی آن را به شکل زیر خواهید داشت:
//DbContextCore : IUnitOfWork

public string EntityHash<TEntity>(TEntity entity) where TEntity : class
{
    var row = Entry(entity).ToDictionary(p => p.Metadata.Name != EFCore.Hash &&
                                              !p.Metadata.ValueGenerated.HasFlag(ValueGenerated.OnUpdate) &&
                                              !p.Metadata.IsShadowProperty());
    return EntityHash<TEntity>(row);
}

protected virtual string EntityHash<TEntity>(Dictionary<string, object> row) where TEntity : class
{
    var json = JsonConvert.SerializeObject(row, Formatting.Indented);
    using (var hashAlgorithm = SHA256.Create())
    {
        var byteValue = Encoding.UTF8.GetBytes(json);
        var byteHash = hashAlgorithm.ComputeHash(byteValue);
        return Convert.ToBase64String(byteHash);
    }
}
همچنین از طریق متدهای الحاقی زیر که مرتبط با واسط IUnitOfWork می‌باشند، امکان دسترسی به رکوردهای دستکاری شده را خواهید داشت:
IsTamperedAsync
HasTamperedEntryAsync
TamperedEntryListAsync

 
6- همانطور که اشاره شد، خواص سایه‌ای مرتبط با سیستم ردیابی موجودیت‌ها نیز به شکل زیر تغییر نام پیدا کرده‌اند:
public const string CreatedDateTime = nameof(ICreationTracking.CreatedDateTime);
public const string CreatedByUserId = nameof(CreatedByUserId);
public const string CreatedByBrowserName = nameof(CreatedByBrowserName);
public const string CreatedByIP = nameof(CreatedByIP);

public const string ModifiedDateTime = nameof(IModificationTracking.ModifiedDateTime);
public const string ModifiedByUserId = nameof(ModifiedByUserId);
public const string ModifiedByBrowserName = nameof(ModifiedByBrowserName);
public const string ModifiedByIP = nameof(ModifiedByIP);
7- یک تبدیلگر سفارشی برای ذخیره سازی اشیا به صورت JSON اضافه شده است که برگرفته از کتابخانه Innofactor.EfCoreJsonValueConverter می‌باشد.
 8- دو متد الحاقی زیر برای نرمال‌سازی خصوصیات تاریخ از نوع DateTime و خصوصیات عددی از نوع Decimal به ModelBuilder اضافه شده‌اند:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{  
    modelBuilder.NormalizeDateTime();
    modelBuilder.NormalizeDecimalPrecision(precision: 20, scale: 6);
    
    base.OnModelCreating(modelBuilder);
}

9-  متد MigrateDbContext به این کتابخانه منتقل شده است:
MigrateDbContext<TContext>(this IHost host)
متد Seed واسط IDbSetup در صورت معرفی یک پیاده‌سازی از آن به سیستم تزریق وابستگی‌ها، در بدنه این متد فراخوانی خواهد شد.

روش معرفی سرویس‌های مرتبط با کتابخانه DNTFrameworkCore.EFCore
services.AddEFCore<ProjectDbContext>()
    .WithTrackingHook<long>()
    .WithDeletedEntityHook()
    .WithRowIntegrityHook()
    .WithNumberingHook(options =>
    {
        options.NumberedEntityMap[typeof(Task)] = new NumberedEntityOption
        {
            Prefix = "Task",
            FieldNames = new[] {nameof(Task.BranchId)}
        };
    });
همانطور که عنوان شد، محدودیت نوع خصوصیات CreatedByUserId و ModifiedByUserId برداشته شده است و از طریق متد WithTrackingHook قابل تنظیم می‎‌باشد.

تغییرات کتابخانه DNTFrameworkCore.Web.Tenancy


فعلا امکان شناسایی مستأجر جاری و دسترسی به اطلاعات آن از طریق واسط ITenantSession در دسترس می‌باشد؛ همچنین امکان تغییر و تعیین رشته اتصال به بانک اطلاعاتی هر مستأجر از طریق متد UseConnectionString واسط IUnitOfWork فراهم می‌باشد.
services.AddTenancy()
    .WithTenantSession()
    .WithStore<InMemoryTenantStore>()
    .WithResolutionStrategy<HostResolutionStrategy>();
app.UseTenancy();


سایر کتابخانه‌ها تغییرات خاصی نداشتند و صرفا نحوه معرفی سرویس‌های آنها ممکن است تغییر کند و یا وابستگی‌های آنها به آخرین نسخه موجود ارتقاء داده شده باشند که در پروژه DNTFrameworkCore.TestAPI اعمال شده‌اند.
لیست بسته‌های نیوگت نسخه ۴.۵.۳
PM> Install-Package DNTFrameworkCore
PM> Install-Package DNTFrameworkCore.EFCore
PM> Install-Package DNTFrameworkCore.EFCore.SqlServer
PM> Install-Package DNTFrameworkCore.Web
PM> Install-Package DNTFrameworkCore.FluentValidation
PM> Install-Package DNTFrameworkCore.Web.Tenancy
PM> Install-Package DNTFrameworkCore.Licensing
مطالب
کار با بانک‌های اطلاعاتی مختلف در PdfReport
تعدادی از منابع داده پیش فرض PdfReport جهت کار مستقیم با بانک‌های اطلاعاتی مختلف، کوئری نوشتن و نمایش نتایج آن‌ها طراحی شده‌اند.
در این بین با توجه به اینکه دات نت پشتیبانی توکاری از SQL Server دارد، اتصال و استفاده از توانمندی‌های آن نیاز به کتابخانه جانبی خاصی ندارد. اما برای کار با بانک‌های اطلاعاتی دیگر نیاز خواهد بود تا پروایدر ADO.NET آن‌ها را تهیه و به برنامه اضافه کنیم.
چهار نمونه از منابع داده پیش فرضی که در متد MainTableDataSource قابل تعریف هستند به شرح زیر می‌باشند:
public void SqlDataReader(string connectionString, string sql, params object[] parametersValues)

//.mdb or .accdb files
public void AccessDataReader(string filePath, string password, string sql, params object[] parametersValues)

public void OdbcDataReader(string connectionString, string sql, params object[] parametersValues)
SqlDataReader برای کار با بانک‌های اطلاعاتی SQL Server بهینه سازی شده است.
AccessDataReader قابلیت اتصال به بانک‌های اطلاعاتی اکسس جدید (فایل‌های accdb) و اکسس قدیم (فایل‌های mdb) را دارد.
OdbcDataReader یک پروایدر عمومی است که از روز اول دات نت به همراه آن بوده است. برای مثال جهت اتصال به بانک‌های اطلاعاتی فاکس‌پرو می‌تواند مورد استفاده قرار گیرد.
اما ... برای مابقی بانک‌های اطلاعاتی چطور؟
برای سایر بانک‌های اطلاعاتی، منبع داده عمومی زیر تدارک دیده شده است:
public void GenericDataReader(string providerName, string connectionString, string sql, params object[] parametersValues)
تنها تفاوت آن با نمونه‌های قبل، ذکر providerName آن است. برای مثال جهت اتصال به SQLite ابتدا پروایدر مخصوص ADO.NET آن‌را دریافت و به پروژه خود اضافه نمائید. سپس پارامتر providerName فوق را با "System.Data.SQLite" مقدار دهی کنید.

یک نکته:
در تمام منابع داده فوق، امکان نوشتن کوئری‌های پارامتری نیز پیش بینی شده است. فقط باید دقت داشت که پارامترهای معرفی شده باید با @ شروع شوند که یک نمونه از آن‌را در مثال جاری ملاحظه خواهید نمود.

در ادامه نحوه تهیه گزارش از یک بانک اطلاعاتی SQLite را توسط PdfReport بررسی خواهیم کرد:

using System;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.SQLiteDataReader
{
    public class SQLiteDataReaderPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.RunDirection(PdfRunDirection.RightToLeft);
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.NumberOfDataRowsPerPage(5);
            })
            .MainTableDataSource(dataSource =>
            {
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("url");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("آدرس");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Hyperlink(foreColor: System.Drawing.Color.Blue, fontUnderline: true);
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("name");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(2);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("NumberOfPosts");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(2);
                    column.HeaderCell("تعداد مطلب");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("AddDate");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("تاریخ ثبت");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : PersianDate.ToPersianDateTime((DateTime)obj) /*((DateTime)obj).ToString("dd/MM/yyyy HH:mm")*/);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptSqlDataReaderSample.pdf"));
        }
    }
}

توضیحات:

- در مثال فوق نحوه استفاده از یک بانک اطلاعاتی SQLite را ملاحظه می‌کنید. این بانک اطلاعاتی نمونه در پوشه bin\data سورس به روز شده پروژه موجود است.
                dataSource.GenericDataReader(
                    providerName: "System.Data.SQLite",
                    connectionString: "Data Source=" + AppPath.ApplicationPath + "\\data\\blogs.sqlite",
                    sql: @"SELECT [url], [name], [NumberOfPosts], [AddDate]
                               FROM [tblBlogs]
                               WHERE [NumberOfPosts]>=@p1",
                    parametersValues: new object[] { 10 }
                );
فرض بر این است که فایل‌های System.Data.SQLite.dll و SQLite.Interop.dll را از سایت SQLite دریافت کرده و سپس ارجاعی را به اسمبلی System.Data.SQLite.dll به پروژه خود افزوده‌اید.
در مرحله بعد به کمک GenericDataReader می‌توان به این پروایدر دسترسی یافت. همانطور که ملاحظه می‌کنید یک کوئری پارامتری با مقدار پارامتر مساوی 10 جهت تهیه گزارش، تعریف شده است.
همچنین باید دقت داشت که اگر پروژه جاری شما مبتنی بر دات نت 4 است، نیاز خواهید داشت چند سطر زیر را به فایل config برنامه اضافه نمائید تا با SQLite مشکلی نداشته باشد:
<?xml version="1.0"?>
   <configuration>
      <startup useLegacyV2RuntimeActivationPolicy="true">
           <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
      </startup>
   </configuration>                  
- مرحله بعد نوبت به معرفی ستون‌های گزارش است. هر ستون، معادل یک فیلد معرفی شده در کوئری SQL ارسال شده به GenericDataReader خواهد بود (کوچکی و بزرگی حروف باید در اینجا رعایت شوند).
- در حین معرفی ستون AddDate، نحوه نمایش و تبدیل تاریخ دریافتی که با فرمت DateTime است را به تاریخ شمسی ملاحظه می‌کنید. متد PersianDate.ToPersianDateTime در فضای نام PdfRpt.Core.Helper قرار دارد. توسط DisplayFormatFormula، فرصت خواهید داشت مقدار متناظر با سلول در حال رندر را پیش از نمایش، به هر نحو دلخواهی فرمت کنید.
- در ستون url از قالب نمایشی پیش فرض Hyperlink، برای نمایش اطلاعات فیلد جاری به صورت یک لینک قابل کلیک استفاده شده است.

یک نکته:
ذکر قسمت MainTableColumns و تمام تعاریف مرتبط با آن در PdfReports اختیاری است. به این معنا که می‌توانید قسمت گزارش سازی و تعاریف گزارشات برنامه خود را پویا کنید (شبیه به حالت auto generate columns در گرید‌های معروف). کوئری‌های SQL متناظر با گزارشات را در بانک اطلاعاتی ذخیره کنید و به گزارش ساز فوق ارسال نمائید. حاصل یک گزارش جدید است.

مطالب
Blazor 5x - قسمت چهارم - مبانی Blazor - بخش 1 - Data Binding
عنوان می‌شود که HTML over Web socket آینده‌ی توسعه‌ی برنامه‌های وب است و این آینده هم اکنون توسط Blazor Server در دسترس است. در این مدل توسعه، ابتدا یک اتصال SignalR برقرار شده و سپس تمام تعاملات بین سرور و کلاینت، از طریق همین اتصال که عموما web socket است، مدیریت می‌شود. به همین جهت در ادامه قصد داریم یک پروژه‌ی Blazor Server را تکمیل کنیم. پس از آن یک پروژه‌ی Blazor WASM را نیز بررسی خواهیم کرد. بنابراین هر دو مدل توسعه‌ی برنامه‌های Blazor را پوشش خواهیم داد. برای این منظور در ابتدا مبانی Blazor را بررسی می‌کنیم که در هر دو مدل یکی است.


تعریف مدل برنامه

در همان پروژه‌ی خالی Blazor Server که در قسمت دوم با دستور dotnet new blazorserver ایجاد کردیم، پوشه‌ی Models را افزوده و کلاس BlazorRoom را در آن تعریف می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public decimal Price { set; get; }

        public bool IsActive { set; get; }
    }
}
سپس برای اینکه مدام نیاز به تعریف فضای نام آن در فایل‌های مختلف razor. برنامه نباشد، به فایل Imports.razor_ مراجعه کرده و سطر زیر را به انتهای آن اضافه می‌کنیم:
@using BlazorServerSample.Models
برنامه را نیز توسط دستور dotnet watch run اجرا می‌کنیم.


Data binding یک طرفه

در ادامه به فایل Pages\Index.razor مراجعه کرده و منهای سطر اول مسیریابی آن، مابقی محتوای آن‌را حذف می‌کنیم. در اینجا می‌خواهیم مقادیر نمونه‌ای از شیء BlazorRoom را نمایش دهیم. به همین جهت این شیء را در قسمت code@ فایل razor جاری (همانند نکات قسمت قبل)، ایجاد می‌کنیم:
@page "/"

<h2 class="bg-light border p-2">
    First Room
</h2>
Room: @Room.Name
<br/>
Price: @Room.Price

@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499
    };
}
در اینجا در ابتدا شیء Room را در قسمت قطعه کد فایل razor جاری ایجاد کرده و سپس اطلاعات آن‌را با استفاده از زبان Razor نمایش داده‌ایم.


 به این روش نمایش اطلاعات، one-way data-binding نیز گفته می‌شود. اما چطور می‌توان یک طرفه بودن آن‌را متوجه شد؟ برای این منظور یک text-box را نیز در ذیل تعاریف فوق، به صورت زیر اضافه می‌کنیم که مقدارش را از Room.Price دریافت می‌کند:
<input type="number" value="@Room.Price" />
اکنون اگر این مقدار را تغییر دهیم، عدد جدید قیمت اتاق، به خاصیت Room.Price منعکس نمی‌شود و تغییری نمی‌کند:



Data binding دو طرفه

اکنون می‌خواهیم اگر مقدار ورودی Room.Price توسط text-box فوق تغییر کرد، نتیجه‌ی نهایی، به خاصیت متناظر با آن نیز اعمال شود و تغییر کند. برای این منظور فقط کافی است ویژگی value را به bind-value@ تغییر دهیم:
<input type="number" @bind-value="@Room.Price" />
ویژگی bind-value@ سبب برقراری data-binding دو طرفه می‌شود. یعنی در ابتدا مقدار اولیه‌ی خاصیت Room.Price را نمایش می‌دهد. در ادامه‌ی اگر کاربر، مقدار این text-box را تغییر داد، نتیجه‌ی نهایی را به خاصیت Room.Price نیز اعمال می‌کند و همچنین این تغییر، سبب به روز رسانی UI نیز می‌شود؛ یعنی در جائیکه پیشتر مقدار اولیه‌ی Room.Price را نمایش داده بودیم، اکنون مقدار جدید آن نمایش داده خواهد شد:


البته اگر برنامه را اجرا کنیم، با تغییر مقدار text-box، بلافاصله تغییری را مشاهده نخواهیم کرد. برای اعمال تغییرات نیاز خواهد بود تا در جائی خارج از text-box کلیک و focus را به المانی دیگر منتقل کنیم. اگر می‌خواهیم همراه با تایپ اطلاعات درون text-box، رابط کاربری نیز به روز شود، می‌توان bind-value را به یک رخداد خاص، مانند oninput متصل کرد. حالت پیش‌فرض آن onchange است:
<input type="number" @bind-value="@Room.Price" @bind-value:event="oninput" />
اکنون اگر برنامه را اجرا کرده و درون text-box اطلاعاتی را وارد کنیم، بلافاصله UI نیز به روز رسانی خواهد شد.
لیست کامل رخ‌دادها را در اینجا می‌توانید مشاهده کنید. برای مثال برای یک المان input، دو رخداد onchange و oninput قابل تعریف هستند.

یک نکته: در حین کار با bind-value@، نیازی نیست مقدار آن با @ شروع شود. یعنی ذکر "bind-value="Room.Price@ نیز کافی است.


تمرین 1 - خاصیت IsActive یک اتاق را به یک checkbox متصل کرده و همچنین وضعیت جاری آن‌را نیز در یک برچسب نمایش دهید.

در اینجا می‌خواهیم مقدار خاصیت Room.IsActive را توسط یک اتصال دو طرفه، به یک checkbox متصل کنیم:
<input type="checkbox" @bind-value="Room.IsActive"  />
<br/>
This room is @(Room.IsActive? "Active" : "Inactive").
با استفاده از bind-value@، وضعیت جاری خاصیت Room.IsActive را به یک checkbox متصل کرده‌ایم. همچنین در ادامه توسط یک عبارت شرطی، این وضعیت را نمایش داده‌ایم.


بار اولی که برنامه نمایش داده می‌شود، هر چند مقدار IsActive بر اساس مقدار دهی آن در شیء Room، مساوی true است، اما chekbox، علامت نخورده باقی می‌ماند. برای رفع این مشکل نیاز است ویژگی checked این المان را نیز به صورت زیر مقدار دهی کرد:
<input type="checkbox" @bind-value="Room.IsActive"
   checked="@(Room.IsActive? "cheked" : null)" />
در این حالت اگر اتاقی فعال باشد، مقدار ویژگی checked، به checked و در غیراینصورت به null تنظیم می‌شود. به این ترتیب مشکل عدم نمایش checkbox انتخاب شده در بار اول نمایش کامپوننت جاری، برطرف می‌شود.


اتصال خواص مدل‌ها به dropdown‌ها

اکنون می‌خواهیم مدل این مثال را کمی توسعه داده و خواص تو در تویی را به آن اضافه کنیم:
using System.Collections.Generic;

namespace BlazorServerSample.Models
{
    public class BlazorRoom
    {
        // ...

        public List<BlazorRoomProp> RoomProps { set; get; }
    }

    public class BlazorRoomProp
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Value { set; get; }
    }
}
برای مثال یک اتاق می‌تواند ویژگی‌هایی مانند مساحت، تعداد نفرات مجاز و غیره را داشته باشد. هدف از ویژگی جدید RoomProps، تعیین لیست این نوع موارد است.
پس از این تعاریف، فیلد Room را به صورت زیر به روز رسانی می‌کنیم تا تعدادی از خواص اتاق را به همراه داشته باشد:
@code
{
    BlazorRoom Room = new BlazorRoom
    {
        Id = 1,
        Name = "Room 1",
        IsActive = true,
        Price = 499,
        RoomProps = new List<BlazorRoomProp>
        {
            new BlazorRoomProp
            {
                Id = 1, Name = "Sq Ft", Value = "100"
            },
            new BlazorRoomProp
            {
                Id = 2, Name = "Occupancy", Value = "3"
            }
        }
    };
}
در ادامه می‌خواهیم این خواص را در یک dropdown نمایش دهیم. همچنین با انتخاب یک خاصیت از دراپ‌داون، مقدار خاصیت انتخابی را در یک برچسب نیز به صورت پویا نمایش خواهیم داد:
<select @bind="SelectedRoomPropValue">
    @foreach (var prop in Room.RoomProps)
    {
        <option value="@prop.Value">@prop.Name</option>
    }
</select>
<span>The value of the selected room prop is: @SelectedRoomPropValue</span>

@code
{
    string SelectedRoomPropValue = "";
    // ...
همانطور که مشاهده می‌کنید، انجام یک چنین کاری با Blazor بسیار ساده‌است و نیازی به استفاده از جاوا اسکریپت و یا جی‌کوئری ندارد.
در اینجا یک فیلد را در قطعه کد برنامه تعریف کرده و به المان select متصل کرده‌ایم. هرگاه آیتمی در این دراپ داون انتخاب شود، این فیلد، مقدار آن آیتم انتخابی را خواهد داشت. در ادامه توسط یک حلقه‌ی foreach، تمام خواص یک اتاق را دریافت کرده و به صورت options‌های یک select استاندارد، نمایش می‌دهیم. در آخر نیز مقدار SelectedRoomPropValue را نمایش داده‌ایم که این مقدار به صورت پویا تغییر می‌کند:



تعریف لیستی از اتاق‌ها

عموما در یک برنامه‌ی واقعی، با یک تک اتاق کار نمی‌کنیم. به همین جهت در ادامه لیستی از اتاق‌ها را تعریف و مقدار دهی اولیه خواهیم کرد:
@code
{
    string SelectedRoomPropValue = "";

    List<BlazorRoom> Rooms = new List<BlazorRoom>();

    protected override void OnInitialized()
    {
        base.OnInitialized();

        Rooms.Add(new BlazorRoom
        {
            Id = 1,
            Name = "Room 1",
            IsActive = true,
            Price = 499,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "100"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "3"
                }
            }
        });

        Rooms.Add(new BlazorRoom
        {
            Id = 2,
            Name = "Room 2",
            IsActive = true,
            Price = 399,
            RoomProps = new List<BlazorRoomProp>
            {
                new BlazorRoomProp
                {
                    Id = 1, Name = "Sq Ft", Value = "250"
                },
                new BlazorRoomProp
                {
                    Id = 2, Name = "Occupancy", Value = "4"
                }
            }
        });
    }
}
در ابتدا فیلد Rooms تعریف شده که لیستی از BlazorRoomها است. در ادامه بجای مقدار دهی مستقیم آن در همان سطح قطعه کد، آن‌را در یک متد life-cycle کامپوننت جاری به نام OnInitialized که مخصوص این نوع مقدار دهی‌های اولیه است، مقدار دهی کرده‌ایم.


نمایش لیست قابل ویرایش اتاق‌ها

اکنون می‌خواهیم به عنوان تمرین 2، لیست جزئیات اتاق‌های تعریف شده را نمایش دهیم؛ با این شرط که نام و قیمت هر اتاق، قابل ویرایش باشد. همچنین خواص تعریف شده نیز به صورت ستون‌هایی مجزا، نمایش داده شوند. برای مثال اگر دو خاصیت در اینجا تعریف شده، 2 ستون اضافه‌تر نیز برای نمایش آن‌ها وجود داشته باشد. به علاوه از آنجائیکه می‌خواهیم اتصال دوطرفه را نیز آزمایش کنیم، نام و قیمت هر اتاق را نیز در پایین جدول، مجددا به صورت برچسب‌هایی نمایش خواهیم داد.


برای رسیدن به تصویر فوق می‌توان به صورت زیر عمل کرد:
<div class="border p-2 mt-3">
    <h2 class="text-info">Rooms List</h2>
    <table class="table table-dark">
        @foreach(var room in Rooms)
        {
            <tr>
                <td>
                    <input type="text" @bind-value="room.Name" @bind-value:event="oninput"/>
                </td>
                <td>
                    <input type="text" @bind-value="room.Price" @bind-value:event="oninput"/>
                </td>
                @foreach (var roomProp in room.RoomProps)
                {
                    <td>
                        @roomProp.Name, @roomProp.Value
                    </td>
                }
            </tr>
        }
    </table>

    @foreach(var room in Rooms)
    {
        <p>@room.Name's price is @room.Price.</p>
    }
</div>
در اینجا یک حلقه‌ی تو در تو را مشاهده می‌کنید. حلقه‌ی بیرونی، ردیف‌های جدول را که شامل نام و قیمت هر اتاق است، به صورت input-boxهای متصل به خواص متناظر با آن‌ها نمایش می‌دهد. سپس برای اینکه بتوانیم خواص هر ردیف را نیز نمایش دهیم، حلقه‌ی دومی را بر روی room.RoomProps تشکیل داده‌ایم.
هدف از foreach پس از جدول، نمایش تغییرات انجام شده‌ی در input-boxها است. برای مثال اگر نام یک ردیف را تغییر دادیم، چون یک اتصال دو طرفه برقرار است، خاصیت متناظر با آن به روز رسانی شده و بلافاصله در برچسب‌های ذیل جدول، منعکس می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-04.zip
مطالب
قابلیت چند زبانه و Localization در AngularJs- بخش چهارم و نهایی: Best Practiceهای angular-translate
در بخش پیشین چند مورد از قابلیت‌های angular-translate را بررسی نمودیم. در این بخش به بررسی باقی موارد می‌پردازیم.

ex7_load_static_files 

در این مثال خواهیم دید که چگونه یک فایل translate table در موقع فراخوانی به صورت On Demand بارگذاری خواهد شد. در قدم اول اسکریپت‌های زیر به صفحه افزوده می‌شوند.
    <script src="Scripts/angular.js"></script>
    <script src="Scripts/angular-cookies.js"></script>
    <script src="Scripts/angular-translate.js"></script>
    <script src="Scripts/angular-translate-storage-cookie.js"></script>
    
    <!-- for override loader methods in angular translate -->
    <script src="/src/service/loader-static-files.js"></script>
در ادامه درباره‌ی اسکریپت پنجم بیشتر توضیح خواهیم داد. بگذارید از آخر به اول شروع کنیم و ببینیم که نحوه‌ی فراخوانی و استفاده از امکان on demand بارگذاری شدن فایل‌های زبان به چه صورتی می‌باشد. در زیر، تکه کد اصافه شده به ex7 را مشاهده می‌کنید:
            // Register a loader for the static files
            // So, the module will search missing translation tables under the specified urls.
            // Those urls are [prefix][langKey][suffix].
            $translateProvider.useStaticFilesLoader({
                prefix: '/l10n/',
                suffix: '.json'
            });
همانطور که در توضیحات آمده است، ماژول با دریافت prefix و suffix که در حقیقت همان فولدر و پسوند فایل‌های translate table هستند، هر زبانی را که مورد نیاز است و تا کنون بارگذاری نشده، بارگذاری می‌نماید. تصویر زیر محتویات فولدر l10n را نمایش می‌دهد.

حال ببینیم که این فرآیند در loader-static-files چگونه پیاده سازی شده است. در این فایل یک متد load نوشته شده است که فایل‌های static را طبق یک الگوی مشتمل بر prefix و suffix از سرور می‌خواند. لزومی ندارد که شما فایل‌ها را حتما به صورت JSON و با این پسوند ذخیره کنید. اما چیزی که قطعی است این است که فایل‌ها حتما باید به صورت key value ذخیره شده باشند.

تکه کد زیر اطلاعات فایل loader-static-files را نمایش می‌دهد.

angular.module('pascalprecht.translate')
.factory('$translateStaticFilesLoader', $translateStaticFilesLoader);
function $translateStaticFilesLoader($q, $http) {
    
  'use strict';

  return function (options) {

    if (!options || (!angular.isArray(options.files) && (!angular.isString(options.prefix) || !angular.isString(options.suffix)))) {
      throw new Error('Couldn\'t load static files, no files and prefix or suffix specified!');
    }

    if (!options.files) {
      options.files = [{
        prefix: options.prefix,
        suffix: options.suffix
      }];
    }

    var load = function (file) {
      if (!file || (!angular.isString(file.prefix) || !angular.isString(file.suffix))) {
        throw new Error('Couldn\'t load static file, no prefix or suffix specified!');
      }

      var deferred = $q.defer();

      $http(angular.extend({
        url: [
          file.prefix,
          options.key,
          file.suffix
        ].join(''),
        method: 'GET',
        params: ''
      }, options.$http)).success(function (data) {
        deferred.resolve(data);
      }).error(function () {
        deferred.reject(options.key);
      });

      return deferred.promise;
    };

    var deferred = $q.defer(),
        promises = [],
        length = options.files.length;

    for (var i = 0; i < length; i++) {
      promises.push(load({
        prefix: options.files[i].prefix,
        key: options.key,
        suffix: options.files[i].suffix
      }));
    }

    $q.all(promises).then(function (data) {
      var length = data.length,
          mergedData = {};

      for (var i = 0; i < length; i++) {
        for (var key in data[i]) {
          mergedData[key] = data[i][key];
        }
      }

      deferred.resolve(mergedData);
    }, function (data) {
      deferred.reject(data);
    });

    return deferred.promise;
  };
}

$translateStaticFilesLoader.displayName = '$translateStaticFilesLoader';

همانطور که ملاحظه می‌کنید، کد فوق یک سرویس با نام $translateStaticFilesLoader را تعریف نموده است. در صورتیکه ما در کنترلر فایل ex7، اصلا نامی از آن نبردیم و تنها از $translateProvider.useStaticFilesLoader استفاده نمودیم! جواب در نحوه‌ی نگارش کد angular-translate نهفته است. در خط 866 فایل angular-translate تکه کد زیر مربوط به تعریف translateStaticFileLoader می‌باشد. همانطور که ملاحظه می‌کنید سرویس translateStaticFilesLoader درون فضای نام سرویس translateTable قرار گرفته است. بنابراین ما تنها با تعریف سرویس translateStaticFilesLoader، در حقیقت آن را override نموده‌ایم. در کد نمونه‌ای که در بخش‌های قبلی قرار داده‌ام یک فایل translate.js نیز قرار دارد که در فولدر src/services قرار گرفته است. این فایل نیز برخی از امکانات و سرویس‌های built-in درون angular-translate را سفارشی نموده است.

  /**
   * @ngdoc function
   * @name pascalprecht.translate.$translateProvider#useStaticFilesLoader
   * @methodOf pascalprecht.translate.$translateProvider
   *
   * @description
   * Tells angular-translate to use `$translateStaticFilesLoader` extension service as loader.
   *
   * @param {Object=} options Optional configuration object
   */
  this.useStaticFilesLoader = function (options) {
    return this.useLoader('$translateStaticFilesLoader', options);
  };

در این 4 مجموعه سعی کردم تمامی آنچه را که برای ایجاد قابلیت چند زبانه و localization نیاز است و حیاتی بود، تشریح کنم. بنابراین تا کنون دانش خوبی درباره‌ی این کتابخانه کسب نموده‌اید. باقی تمرین‌ها را می‌توانید بر حسب نیاز با استفاده از مستندات موجود در angular-translate مطالعه و استفاده نمایید.

مطالب
استفاده از jQuery Ajax جهت تعیین اعتبار یک فرم

فرض کنید تعیین اعتبار یکی از فیلدهای فرم نیاز به انجام محاسباتی در سمت سرور دارد و این‌کار را می‌خواهیم با استفاده از jQuery Ajax‌ انجام دهیم. مشکلی که در اینجا وجود دارد، این است که A در Ajax به معنای asynchronous است. یعنی زمانیکه کاربر دکمه submit را فشرد، دیگر برنامه منتظر این نخواهد شد که پاسخ کامل دریافت شود ، سایر پردازش‌ها صورت گیرد و سپس فرم را به سرور ارسال نماید (شبیه به ایجاد یک ترد جدید در برنامه‌های ویندوزی). مثال زیر را در نظر بگیرید:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestCustomValidation.aspx.cs"
Inherits="TestJQueryAjax.TestCustomValodation" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="js/jquery.js" type="text/javascript"></script>

<script type="text/javascript">
function validate() {
var number1 = $("#<%=txtNumber1.ClientID %>").val();
var number2 = $("#<%=txtNumber2.ClientID %>").val();
var result = false;
$.ajax({
type: "POST",
url: 'AjaxSrv.asmx/ValidateIt',
data: '{"number1":' + number1 + ',"number2":' + number2 + '}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
if (msg.d) {
result = true;
alert('بسیار خوب');
}
else {
result = false;
alert('دوباره سعی کنید');
}
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
result = false;
alert("خطایی رخ داده است");
}
});
//debugger;
return result;
}
</script>

</head>
<body>
<form id="form1" runat="server">
<div>
number 1 :
<asp:TextBox runat="server" ID="txtNumber1" />
<br />
number 2 :
<asp:TextBox runat="server" ID="txtNumber2" />
<br />
<asp:Button ID="btnSubmit" Text="Submit" UseSubmitBehavior="false" runat="server"
OnClientClick="if(!validate()){ return false;}" OnClick="btnSubmitClick" />
</div>
</form>
</body>
</html>

این مثال یک نوع اعتبار سنجی سفارشی را در حین submit با استفاده از وب سرویس زیر انجام می‌دهد (حاصلضرب دو عدد دریافتی را بررسی می‌کند که باید مساوی 10 باشند. البته هدف از این مثال ساده، آشنایی با نحوه‌ی انجام این نوع عملیات است که می‌تواند شامل کار با دیتابیس و غیره هم باشد. و گرنه بدیهی است این بررسی را با دو سطر کد جاوا اسکریپتی نیز می‌شد انجام داد):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Script.Services;

namespace TestJQueryAjax
{
/// <summary>
/// Summary description for AjaxSrv
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[ScriptService]
public class AjaxSrv : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public bool ValidateIt(int number1, int number2)
{
return number1 * number2 == 10;
}
}
}
در این مثال هنگام submit ، عملیات اعتبار سنجی با توجه به وضعیت asynchronous عملیات Ajax ، تمام و کمال رخ نداده ، یا اعتبار سنجی انجام نمی‌شود و یا پیغام خطایی را دریافت خواهیم کرد.

راه حل چیست؟
راه حل‌های فضایی بسیاری را در وب در این مورد می‌توان پیدا کرد؛ اما راه حل استاندارد آن در این حالت ویژه، استفاده از Ajax در حالت غیرهمزمان است. یعنی این فریم ورک Ajax را وادار کنیم که تا پایان عملیات مورد نظر، منتظر بماند و سپس فرم را ارسال کند. برای این منظور تنها کافی است یک سطر زیر را پیش از فراخوانی تابع Ajax ، اضافه و فراخوانی نمائیم:

$.ajaxSetup({async: false}) ;
نکته:
UseSubmitBehavior دکمه ما را به شکل زیر رندر می‌کند (دکمه به یک button معمولی (بجای حالت submit) تبدیل شده و سپس یک doPostBack را اضافه خواهد کرد):

<input id="btnSubmit" type="button" onclick="if(!validate()){ return false;};__doPostBack('btnSubmit','')" value="Submit" name="btnSubmit"/>


مطالب
ASP.NET MVC #21

آشنایی با تکنیک‌های Ajax در ASP.NET MVC

اهمیت آشنایی با Ajax، ارائه تجربه‌ کاربری بهتری از برنامه‌های وب، به مصرف کنندگان نهایی آن می‌باشد. به این ترتیب می‌توان درخواست‌های غیرهمزمانی (asynchronous) را با فرمت XML یا Json به سرور ارسال کرد و سپس نتیجه نهایی را که حجم آن نسبت به یک صفحه کامل بسیار کمتر است، به کاربر ارائه داد. غیرهمزمان بودن درخواست‌ها سبب می‌شود تا ترد اصلی رابط کاربری برنامه قفل نشده و کاربر در این بین می‌تواند به سایر امور خود بپردازد. به این ترتیب می‌توان برنامه‌های وبی را که شبیه به برنامه‌های دسکتاپ هستند تولید نمود؛ کل صفحه مرتبا به سرور ارسال نمی‌شود، flickering و چشمک زدن صفحه کاهش خواهد یافت (چون نیازی به ترسیم مجدد کل صفحه نخواهد بود و عموما قسمتی جزئی از یک صفحه به روز می‌شود) یا بدون نیاز به ارسال کل صفحه به سرور، به کاربری خواهیم گفت که آیا اطلاعاتی که وارد کرده است معتبر می‌باشد یا نه (نمونه‌ای از آن‌ را در قسمت Remote validation اعتبار سنجی اطلاعات ملاحظه نمودید).


مروری بر محتویات پوشه Scripts یک پروژه جدید ASP.NET MVC در ویژوال استودیو

با ایجاد هر پروژه ASP.NET MVC‌ جدیدی در ویژوال استودیو، یک سری اسکریپت‌ هم به صورت خودکار در پوشه Scripts آن اضافه می‌شوند. تعدادی از این فایل‌ها توسط مایکروسافت پیاده سازی شده‌اند. برای مثال:
MicrosoftAjax.debug.js
MicrosoftAjax.js
MicrosoftMvcAjax.debug.js
MicrosoftMvcAjax.js
MicrosoftMvcValidation.debug.js
MicrosoftMvcValidation.js

این فایل‌ها از ASP.NET MVC 3 به بعد، صرفا جهت سازگاری با نگارش‌های قبلی قرار دارند و استفاده از آن‌ها اختیاری است. بنابراین با خیال راحت آن‌ها را delete کنید! روش توصیه شده جهت پیاده سازی ویژگی‌های Ajax ایی، استفاده از کتابخانه‌های مرتبط با jQuery می‌باشد؛ از این جهت که 100ها افزونه برای کار با آن توسط گروه وسیعی از برنامه نویس‌ها در سراسر دنیا تاکنون تهیه شده است. به علاوه فریم ورک jQuery تنها منحصر به اعمال Ajax ایی نیست و از آن جهت دستکاری DOM (document object model) و CSS صفحه نیز می‌توان استفاده کرد. همچنین حجم کمی نیز داشته،‌ با انواع و اقسام مرورگرها سازگار است و مرتبا هم به روز می‌شود.

در این پوشه سه فایل دیگر پایه کتابخانه jQuery نیز قرار دارند:
jquery-xyz-vsdoc.js
jquery-xyz.js
jquery-xyz.min.js

فایل vsdoc برای ارائه نهایی برنامه طراحی نشده است. هدف از آن ارائه Intellisense بهتری از jQuery در VS.NET می‌باشد. فایلی که باید به کلاینت ارائه شود، فایل min یا فشرده شده آن است. اگر به آن نگاهی بیندازیم به نظر obfuscated مشاهده می‌شود. علت آن هم حذف فواصل، توضیحات و همچنین کاهش طول متغیرها است تا اندازه فایل نهایی به حداقل خود کاهش پیدا کند. البته این فایل از دیدگاه مفسر جاوا اسکریپت یک مرورگر، فایل بی‌نقصی است!
اگر علاقمند هستید که سورس اصلی jQuery را مطالعه کنید، به فایل jquery-xyz.js مراجعه نمائید.
محل الحاق اسکریپت‌های عمومی مورد نیاز برنامه نیز بهتر است در فایل master page یا layout برنامه باشد که به صورت پیش فرض اینکار انجام شده است.
سایر فایل‌های اسکریپتی که در این پوشه مشاهده می‌شوند، یک سری افزونه عمومی یا نوشته شده توسط تیم ASP.NET MVC برفراز jQuery هستند.

به چهار نکته نیز حین استفاده از اسکریپت‌های موجود باید دقت داشت:
الف) همیشه از متد Url.Content همانند تعاریفی که در فایل Views\Shared\_Layout.cshtml مشاهده می‌کنید،‌ برای مشخص سازی مسیر ریشه سایت، استفاده نمائید. به این ترتیب صرفنظر از آدرس جاری صفحه، همواره آدرس صحیح قرارگیری پوشه اسکریپت‌ها در صفحه ذکر خواهد شد.
ب) ترتیب فایل‌های js مهم هستند. ابتدا باید کتابخانه اصلی jQuery ذکر شود و سپس افزونه‌های آن‌ها.
ج) اگر اسکریپت‌های jQuery در فایل layout سایت تعریف شده‌اند؛ نیازی به تعریف مجدد آن‌ها در View‌های سایت نیست.
د) اگر View ایی به اسکریپت ویژه‌ای جهت اجرا نیاز دارد، بهتر است آن‌را به شکل یک section داخل view تعریف کرد و سپس به کمک متد RenderSection این قسمت را در layout سایت مقدار دهی نمود. مثالی از آن‌را در قسمت 20 این سری مشاهده نمودید (افزودن نمایش جمع هر ستون گزارش).


یک نکته
اگر آخرین به روز رسانی‌های ASP.NET MVC را نیز نصب کرده باشید، فایلی به نام packages.config به صورت پیش فرض به هر پروژه جدید ASP.NET MVC اضافه می‌شود. به این ترتیب VS.NET به کمک NuGet این امکان را خواهد یافت تا شما را از آخرین به روز رسانی‌های این کتابخانه‌ها مطلع کند.


آشنایی با Ajax Helpers توکار ASP.NET MVC

اگر به تعاریف خواص و متدهای کلاس WebViewPage دقت کنیم:

using System;

namespace System.Web.Mvc
{
public abstract class WebViewPage<TModel> : WebViewPage
{
protected WebViewPage();
public AjaxHelper<TModel> Ajax { get; set; }
public HtmlHelper<TModel> Html { get; set; }
public TModel Model { get; }
public ViewDataDictionary<TModel> ViewData { get; set; }
public override void InitHelpers();
protected override void SetViewData(ViewDataDictionary viewData);
}
}

علاوه بر خاصیت Html که وهله‌ای از آن امکان دسترسی به Html helpers توکار ASP.NET MVC را در یک View فراهم می‌کند، خاصیتی به نام Ajax نیز وجود دارد که توسط آن می‌توان به تعدادی متد AjaxHelper توکار دسترسی داشت. برای مثال توسط متد Ajax.ActionLink می‌توان قسمتی از صفحه را به کمک ویژگی‌های Ajax، به روز رسانی کرد.


مثالی در مورد به روز رسانی قسمتی از صفحه به کمک متد Ajax.ActionLink

ابتدا نیاز است فایل Views\Shared\_Layout.cshtml را اندکی ویرایش کرد. برای این منظور سطر الحاق jquery.unobtrusive-ajax.min.js را به فایل layout برنامه اضافه نمائید (اگر این سطر اضافه نشود، متد Ajax.ActionLink همانند یک لینک معمولی رفتار خواهد کرد):

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
</head>

سپس مدل ساده و منبع داده زیر را نیز به پروژه اضافه کنید:

namespace MvcApplication18.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Collections.Generic;

namespace MvcApplication18.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
for (int i = 0; i < 1000; i++)
{
list.Add(new Employee { Id = i + 1, Name = "name " + i });
}
return list;
}
}
}

در ادامه کنترلر جدیدی را به برنامه با محتوای زیر اضافه کنید:

using System.Linq;
using System.Web.Mvc;
using MvcApplication18.Models;

namespace MvcApplication18.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpPost] //for IE-8
public ActionResult EmployeeInfo(int? id)
{
if (!Request.IsAjaxRequest())
return View("Error");

if (!id.HasValue)
return View("Error");

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return View("Error");

return PartialView(viewName: "_EmployeeInfo", model: data);
}
}
}

بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. یک View خالی را به آن اضافه نمائید. همچنین بر روی متد EmployeeInfo کلیک راست کرده و با انتخاب گزینه Add view در صفحه ظاهر شده یک partial view را اضافه نمائید. جهت تمایز بین partial view و view هم بهتر است نام partial view با یک underline شروع شود.
کدهای partial view مورد نظر را به نحو زیر تغییر دهید:

@model MvcApplication18.Models.Employee

<strong>Name:</strong> @Model.Name

سپس کدهای View متناظر با متد Index را نیز به صورت زیر اعمال کنید:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>

<div id="EmployeeInfo">
@Ajax.ActionLink(
linkText: "Get Employee-1 info",
actionName: "EmployeeInfo",
controllerName: "Home",
routeValues: new { id = 1 },
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
})
</div>

<div id="Progress" style="display: none">
<img src="@Url.Content("~/Content/images/loading.gif")" alt="loading..." />
</div>

توضیحات جزئیات کدهای فوق

متد Ajax.ActionLink لینکی را تولید می‌کند که با کلیک کاربر بر روی آن، اطلاعات اکشن متد واقع در کنترلری مشخص، به کمک ویژگی‌های jQuery Ajax دریافت شده و سپس در مقصدی که توسط UpdateTargetId مشخص می‌گردد، بر اساس مقدار InsertionMode،‌ درج خواهد شد (می‌تواند قبل از آن درج شود یا پس از آن و یا اینکه کل محتوای مقصد را بازنویسی کند). HttpMethod آن هم به POST تنظیم شده تا با IE‌ مشکلی نباشد. از این جهت که IE پیغام‌های GET را کش می‌کند و مساله ساز خواهد شد. توسط پارامتر routeValues، آرگومان مورد نظر به متد EmployeeInfo ارسال خواهد شد.
به علاوه یکی دیگر از خواص کلاس AjaxOptions، برای معرفی حالت بروز خطایی در سمت سرور به نام OnFailure در نظر گرفته شده است. در اینجا می‌توان نام یک متد JavaScript ایی را مشخص کرده و پیغام خطای عمومی را در صورت فراخوانی آن به کاربر نمایش داد. یا توسط خاصیت Confirm آن می‌توان یک پیغام را پیش از ارسال اطلاعات به سرور به کاربر نمایش داد.
به این ترتیب در مثال فوق، id=1 به متد EmployeeInfo به صورت غیرهمزمان ارسال می‌گردد. سپس کارمندی بر این اساس یافت شده و در ادامه partial view مورد نظر بر اساس اطلاعات کاربر مذکور، رندر خواهد شد. نتیجه کار، در یک div با id مساوی EmployeeInfo درج می‌گردد (InsertionMode.Replace). متد Ajax.ActionLink از این جهت داخل div تعریف شده‌است که پس از کلیک کاربر و جایگزینی محتوا، محو شود. اگر نیازی به محو آن نبود، آن‌را خارج از div تعریف کنید.
عملیات دریافت اطلاعات از سرور ممکن است مدتی طول بکشد (برای مثال دریافت اطلاعات از بانک اطلاعاتی). به همین جهت بهتر است در این بین از تصاویری که نمایش دهنده انجام عملیات است، استفاده شود. برای این منظور یک div با id مساوی Progress تعریف شده و id آن به LoadingElementId انتساب داده شده است. این div با توجه به display: none آن، در ابتدای امر به کاربر نمایش داده نخواهد شد؛ در آغاز کار دریافت اطلاعات از سرور توسط متد Ajax.ActionLink نمایان شده و پس از خاتمه کار مجددا مخفی خواهد شد.
به علاوه اگر به کدهای فوق دقت کرده باشید، از متد Request.IsAjaxRequest نیز استفاده شده است. به این ترتیب می‌توان تشخیص داد که آیا درخواست رسیده از طرف jQuery Ajax صادر شده است یا خیر. البته آنچنان روش قابل ملاحظه‌ای نیست؛ چون امکان دستکاری Http Headers همیشه وجود دارد؛ اما بررسی آن ضرری ندارد. البته این نوع بررسی‌ها را در ASP.NET MVC بهتر است تبدیل به یک فیلتر سفارشی نمود؛ به این ترتیب حجم if و else نویسی در متدهای کنترلرها به حداقل خواهد رسید. برای مثال:

[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method)]
public class AjaxOnlyAttribute : ActionFilterAttribute
{
  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    if (filterContext.HttpContext.Request.IsAjaxRequest())
    {
      base.OnActionExecuting(filterContext);
    }
    else
    {
      throw new InvalidOperationException("This operation can only be accessed via Ajax requests");
    }
  }
}

و برای استفاده از آن خواهیم داشت:

[AjaxOnly]
public ActionResult SomeAjaxAction()
{
    return Content("Hello!");
}


در مورد کلمه unobtrusive در قسمت بررسی نحوه اعتبار سنجی اطلاعات، توضیحاتی را ملاحظه نموده‌اید. در اینجا نیز از ویژگی‌های data-* برای معرفی پارامترهای مورد نیاز حین ارسال اطلاعات به سرور، استفاده می‌گردد. برای مثال خروجی متد Ajax.ActionLink به شکل زیر است. به این ترتیب امکان حذف کدهای جاوا اسکریپت از صفحه فراهم می‌شود و توسط یک فایل jquery.unobtrusive-ajax.min.js که توسط تیم ASP.NET MVC تهیه شده، اطلاعات مورد نیاز به سرور ارسال خواهد گردید:
<a data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" 
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
href="/Home/EmployeeInfo/1">Get Employee-1 info</a>

در کل این روش قابلیت نگهداری بهتری نسبت به روش اسکریپت نویسی مستقیم داخل صفحات را به همراه دارد. به علاوه جدا سازی افزونه اسکریپت وفق دهنده این اطلاعات با متد jQuery.Ajax از صفحه جاری، که امکان کش شدن آن‌را به سادگی میسر می‌سازد.


به روز رسانی اطلاعات قسمتی از صفحه بدون استفاده از متد Ajax.ActionLink

الزامی به استفاده از متد Ajax.ActionLink و فایل jquery.unobtrusive-ajax.min.js وجود ندارد. اینکار را مستقیما به کمک jQuery نیز می‌توان به نحو زیر انجام داد:

<a href="#" onclick="LoadEmployeeInfo()">Get Employee-1 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfo() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfo",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning a simple text, not json
complete: function (xhr, status) {
var data = xhr.responseText;
if (status === 'error' || !data) {
//handleError
}
else {
$('#EmployeeInfo').html(data);
}
hideProgress();
}
});
}
function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

توضیحات:
توسط متد jQuery.Ajax نیز می‌توان درخواست‌های Ajax ایی خود را به سرور ارسال کرد. در اینجا type نوع http verb مورد نظر را مشخص می‌کند که به POST تنظیم شده است. Url آدرس کنترلر را دریافت می‌کند. البته حین استفاده از متد توکار Ajax.ActionLink،‌ این لینک به صورت خودکار بر اساس تعاریف مسیریابی برنامه تنظیم می‌شود. اما در صورت استفاده مستقیم از jQuery.Ajax باید دقت داشت که با تغییر تعاریف مسیریابی برنامه نیاز است تا این Url نیز به روز شود.
سه سطر بعدی نوع اطلاعاتی را که باید به سرور POST شوند مشخص می‌کند. نوع json است و همچنین contentType آن برای ارسال اطلاعات یونیکد ضروری است. از متد JSON.stringify برای تبدیل اشیاء به رشته کمک گرفته‌ایم. این متد در تمام مرورگرهای امروزی به صورت توکار پشتیبانی می‌شود و استفاده از آن سبب خواهد شد تا اطلاعات به نحو صحیحی encode شده و به سرور ارسال شوند. بنابراین این رشته ارسالی اطلاعات را به صورت دستی تهیه نکنید؛ چون کاراکترهای زیادی هستند که ممکن است مشکل ساز شده و باید پیش از ارسال به سرور اصطلاحا escape یا encode شوند.
متداول است از پارامتر success برای دریافت نتیجه عملیات متد jQuery.Ajax استفاده شود. اما در اینجا از پارامتر complete آن استفاده شده است. علت هم اینجا است که return PartialView یک رشته را بر می‌گرداند. پارامتر success انتظار دریافت خروجی از نوع json را دارد. به همین جهت در این مثال خاص باید از پارامتر complete استفاده کرد تا بتوان به رشته بدون فرمت خروجی بدون مشکل دسترسی پیدا کرد.
به علاوه چون از یک section برای تعریف اسکریپت‌های مورد نیاز استفاده کرده‌ایم، برای درج خودکار آن در هدر صفحه باید قسمت هدر فایل layout برنامه را به صورت زیر مقدار دهی کرد:

@RenderSection("javascript", required: false)



دسترسی به اطلاعات یک مدل در View، به کمک jQuery Ajax

اگر جزئی از صفحه که قرار است به روز شود، پیچیده است، روش استفاده از partial viewها توصیه می‌شود؛ برای مثال می‌توان اطلاعات یک مدل را به همراه یک گرید کامل از اطلاعات، رندر کرد و سپس در صفحه درج نمود. اما اگر تنها به اطلاعات چند خاصیت از مدلی نیاز داشتیم، می‌توان از روش‌هایی با سربار کمتر نیز استفاده کرد. برای مثال متد جدید زیر را به کنترلر Home اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult EmployeeInfoData(int? id)
{
if (!Request.IsAjaxRequest())
return Json(false);

if (!id.HasValue)
return Json(false);

var list = EmployeeDataSource.CreateEmployees();
var data = list.Where(x => x.Id == id.Value).FirstOrDefault();
if (data == null)
return Json(false);

return Json(data);
}

سپس View برنامه را نیز به نحو زیر تغییر دهید:

<a href="#" onclick="LoadEmployeeInfoData()">Get Employee-2 info</a>
@section javascript
{
<script type="text/javascript">
function LoadEmployeeInfoData() {
showProgress();
$.ajax({
type: "POST",
url: "/Home/EmployeeInfoData",
data: JSON.stringify({ id: 1 }),
contentType: "application/json; charset=utf-8",
dataType: "json",
// controller is returning the json data
success: function (result) {
if (result) {
alert(result.Id + ' - ' + result.Name);
}
hideProgress();
},
error: function (result) {
alert(result.status + ' ' + result.statusText);
hideProgress();
}
});
}

function showProgress() {
$('#Progress').css("display", "block");
}
function hideProgress() {
$('#Progress').css("display", "none");
}
</script>
}

در این مثال، کنترلر برنامه، اطلاعات مدل را تبدیل به Json کرده و بازگشت خواهد داد. سپس می‌توان به اطلاعات این مدل و خواص آن در View برنامه، در پارامتر success متد jQuery.Ajax، مطابق کدهای فوق دسترسی یافت. اینبار چون خروجی کنترلر تعریف شده از نوع Json است، امکان استفاده از پارامتر success فراهم شده است. همه چیز هم در اینجا خودکار است؛ تبدیل یک شیء به Json و برعکس.
یک نکته: اگر نوع متد کنترلر، HttpGet باشد، نیاز خواهد بود تا پارامتر دوم متد بازگشت Json، مساوی JsonRequestBehavior.AllowGet قرار داده شود.


ارسال اطلاعات فرم‌ها به سرور، به کمک ویژگی‌های Ajax

متد کمکی توکار دیگری به نام Ajax.BeginForm در ASP.NET MVC وجود دارد که کار ارسال غیرهمزمان اطلاعات یک فرم را به سرور انجام داده و سپس اطلاعاتی را از سرور دریافت و قسمتی از صفحه را به روز خواهد کرد. مکانیزم کاری کلی آن بسیار شبیه به متد Ajax.ActionLink می‌باشد. در ادامه با تکمیل مثال قسمت جاری، به بررسی این ویژگی خواهیم پرداخت.
ابتدا متد جستجوی زیر را به کنترلر برنامه اضافه کنید:

[HttpPost] //for IE-8        
public ActionResult SearchEmployeeInfo(string data)
{
if (!Request.IsAjaxRequest())
return Content(string.Empty);

if (string.IsNullOrWhiteSpace(data))
return Content(string.Empty);

var employeesList = EmployeeDataSource.CreateEmployees();
var list = employeesList.Where(x => x.Name.Contains(data)).ToList();
if (list == null || !list.Any())
return Content(string.Empty);

return PartialView(viewName: "_SearchEmployeeInfo", model: list);
}

سپس بر روی نام متد کلیک راست کرده و گزینه add view را انتخاب کنید. در صفحه باز شده، گزینه create a stronlgly typed view را انتخاب کرده و قالب scaffolding را هم بر روی list قرار دهید. سپس گزینه ایجاد partial view را نیز انتخاب کنید. نام آن‌را هم _SearchEmployeeInfo وارد نمائید. برای نمونه خروجی حاصل به نحو زیر خواهد بود:

@model IEnumerable<MvcApplication18.Models.Employee>

<table>
<tr>
<th>
Name
</th>
</tr>

@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
</tr>
}

</table>

تا اینجا یک متد جستجو را ایجاد کرده‌ایم که می‌تواند لیستی از رکوردهای کارمندان را بر اساس قسمتی از نام آن‌ها که توسط کاربری جستجو شده است، بازگشت دهد. سپس این اطلاعات را به partial view مورد نظر ارسال کرده و یک جدول را بر اساس آن تولید خواهیم نمود.
اکنون به فایل Index.cshtml مراجعه کرده و فرم Ajax ایی زیر را اضافه نمائید:

@using (Ajax.BeginForm(actionName: "SearchEmployeeInfo",
controllerName: "Home",
ajaxOptions: new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EmployeeInfo",
LoadingElementId = "Progress"
}))
{
@Html.TextBox("data")
<input type="submit" value="Search" />
}

اینبار بجای استفاده از متد Html.BeginForm از متد Ajax.BeginForm استفاده شده است. به کمک آن اطلاعات Html.TextBox تعریف شده، به کنترلر Home و متد SearchEmployeeInfo آن، بر اساس HttpMethod تعریف شده، ارسال گردیده و نتیجه آن در یک div با id مساوی EmployeeInfo درج می‌گردد. همچنین اگر اطلاعاتی یافت نشد، به کمک متد return Content یک رشته خالی بازگشت داده می‌شود.
متد Ajax.BeginForm نیز از ویژگی‌های data-* برای تعریف اطلاعات مورد نیاز ارسالی به سرور استفاده می‌کند. بنابراین نیاز به سطر الحاق jquery.unobtrusive-ajax.min.js در فایل layout برنامه جهت وفق دادن این اطلاعات unobtrusive به اطلاعات مورد نیاز متد jQuery.Ajax وجود دارد.

<form action="/Home/SearchEmployeeInfo" data-ajax="true" 
data-ajax-loading="#Progress" data-ajax-method="POST"
data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"
id="form0" method="post">
<input id="data" name="data" type="text" value="" />
<input type="submit" value="Search" />
</form>


کتابخانه کمکی «ASP.net MVC Awesome - jQuery Ajax Helpers»
علاوه بر متدهای توکار Ajax همراه با ASP.NET MVC، سایر علاقمندان نیز یک سری Ajax helper را بر اساس افزونه‌های jQuery تدارک دیده‌اند که از آدرس زیر قابل دریافت هستند:
http://awesome.codeplex.com/


افزودن فرم‌ها به کمک jQuery.Ajax و فعال سازی اعتبار سنجی سمت کلاینت

در ASP.NET MVC چون ViewState حذف شده است، امکان تزریق فرم‌های جدید به صفحه یا به روز رسانی قسمتی از صفحه توسط jQuery Ajax به سهولت و بدون دریافت پیغام «viewstate is corrupted» در حین ارسال اطلاعات به سرور، میسر است.
در این حالت باید به یک نکته مهم نیز دقت داشت: «اعتبار سنجی سمت کلاینت دیگر کار نمی‌کند». علت اینجا است که در حین بارگذاری متداول یک صفحه، متد زیر به صورت خودکار فراخوانی می‌گردد:
$.validator.unobtrusive.parse("#{form-id}");

اما با به روز رسانی قسمتی از صفحه، دیگر اینچنین نخواهد بود و نیاز است این فراخوانی را دستی انجام دهیم. برای مثال:

$.ajax
({
url: "/{controller}/{action}/{id}",
type: "get",
success: function(data)
{
$.validator.unobtrusive.parse("#{form-id}");
}
});

//or
$.get('/{controller}/{action}/{id}', function (data) { $.validator.unobtrusive.parse("#{form-id}"); });

شبیه به همین مساله را حین استفاده از Ajax.BeginForm نیز باید مد نظر داشت:

@using (Ajax.BeginForm(
    "Action1",
    "Controller",
    null,
    new AjaxOptions {
        OnSuccess = "onSuccess",
        UpdateTargetId = "result"
    },
    null)
)
{
    <input type="submit" value="Save" />
}

var onSuccess = function(result) {
    // enable unobtrusive validation for the contents
    // that was injected into the <div id="result"></div> node
    $.validator.unobtrusive.parse("#result");
};

در این مثال در پارامتر UpdateTargetId، مجددا یک فرم رندر می‌شود. بنابراین اعتبار سنجی سمت کلاینت آن دیگر کار نخواهد کرد مگر اینکه با مقدار دهی خاصیت OnSuccess، مجددا متد unobtrusive.parse را فراخوانی کنیم.


مطالب
ذخیره سازی تنظیمات برنامه‌های ASP.NET Core در بانک اطلاعاتی به کمک Entity Framework Core
در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config » با مقدمات کار با فایل‌های تنظیمات برنامه و تامین کننده‌های مختلف آن‌ها آشنا شدیم. در این مطلب قصد داریم یک نمونه‌ی سفارشی تامین کننده‌های تنظیمات برنامه را بر اساس دریافت و ذخیره سازی اطلاعات در بانک اطلاعاتی، تهیه کنیم.


ساختار موجودیت تنظیمات برنامه

تنظیمات برنامه با هر قالبی که تهیه شوند، دست آخر به صورت یک <Dictionary<string,string در برنامه پردازش شده و قابل دسترسی می‌شوند. بنابراین موجودیت معادل این Dictionary را به صورت زیر تعریف می‌کنیم:
namespace DbConfig.Web.DomainClasses
{
    public class ConfigurationValue
    {
        public int Id { get; set; }
        public string Key { get; set; }
        public string Value { get; set; }
    }
}


ساختار Context برنامه و مقدار دهی اولیه‌ی آن

پس از تعریف موجودیت تنظیمات برنامه، آن‌را به صورت زیر به Context برنامه معرفی می‌کنیم:
    public class MyAppContext : DbContext, IUnitOfWork
    {
        public MyAppContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<ConfigurationValue> Configurations { set; get; }
همچنین، برای مقدار دهی مقادیر اولیه‌ی تنظیمات برنامه نیز اینبار می‌توان به کمک متد HasData، به صورت زیر عمل کرد:
        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<ConfigurationValue>(entity =>
            {
                entity.Property(e => e.Key).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Key).IsUnique();
                entity.Property(e => e.Value).IsRequired();
                entity.HasData(new ConfigurationValue
                {
                    Id = 1,
                    Key = "key-1",
                    Value = "value_from_ef_1"
                });
                entity.HasData(new ConfigurationValue
                {
                    Id = 2,
                    Key = "key-2",
                    Value = "value_from_ef_2"
                });
            });
        }

ایجاد یک IConfigurationSource سفارشی مبتنی بر بانک اطلاعاتی

انواع و اقسام تامین کننده‌های تنظیمات برنامه در پروژه‌های ASP.NET Core، در حقیقت یک پیاده سازی سفارشی از اینترفیس IConfigurationSource هستند. به همین جهت در ادامه یک نمونه‌ی مبتنی بر EF Core آن را تهیه می‌کنیم:
    public class EFConfigurationSource : IConfigurationSource
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationSource(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }

        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(_serviceProvider);
        }
    }
در اینجا چون می‌خواهیم به IUnitOfWork دسترسی پیدا کنیم، IServiceProvider را به سازنده‌ی این تامین کننده تزریق کرده‌ایم. کار اصلی ساخت آن نیز در متد Build، با ارائه‌ی یک IConfigurationProvider سفارشی انجام می‌شود. اینجا است که اطلاعات را از بانک اطلاعاتی خوانده و در اختیار سیستم تنظیمات برنامه قرار می‌دهیم:
    public class EFConfigurationProvider : ConfigurationProvider
    {
        private readonly IServiceProvider _serviceProvider;

        public EFConfigurationProvider(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
            ensureDatabaseIsCreated();
        }

        public override void Load()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                this.Data?.Clear();
                this.Data = uow.Set<ConfigurationValue>()
                               .AsNoTracking()
                               .ToList()
                               .ToDictionary(c => c.Key, c => c.Value);
            }
        }

        private void ensureDatabaseIsCreated()
        {
            using (var scope = _serviceProvider.CreateScope())
            {
                var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
                uow.Migrate();
            }
        }
    }
در ConfigurationProvider فوق، متد Load، در آغاز برنامه فراخوانی شده و در اینجا فرصت داریم تا خاصیت this.Data آن‌را که از نوع <Dictionary<string,string است، مقدار دهی کنیم. بنابراین از serviceProvider تزریق شده‌ی در سازنده‌ی کلاس استفاده کرده و به وهله‌ای از IUnitOfWork دسترسی پیدا می‌کنیم. سپس بر این اساس تمام رکوردهای جدول متناظر با ConfigurationValue را دریافت و توسط متد ToDictionary، تبدیل به ساختار مدنظر خاصیت this.Data می‌کنیم.
در اینجا فراخوانی متد ensureDatabaseIsCreated را نیز مشاهده می‌کنید. کلاس EFConfigurationProvider در آغاز برنامه و پیش از هر عمل دیگری وهله سازی شده و سپس متد Load آن فراخوانی می‌شود. به همین جهت نیاز است یا پیشتر، بانک اطلاعاتی را توسط دستورات Migration ایجاد کرده باشید و یا متد ensureDatabaseIsCreated، اطلاعات Migration موجود را به بانک اطلاعاتی برنامه اعمال می‌کند.


معرفی EFConfigurationSource به برنامه

جهت معرفی ساده‌تر EFConfigurationSource تهیه شده، ابتدا یک متد الحاقی را بر اساس آن تهیه می‌کنیم:
    public static class EFExtensions
    {
        public static IConfigurationBuilder AddEFConfig(this IConfigurationBuilder builder,
            IServiceProvider serviceProvider)
        {
            return builder.Add(new EFConfigurationSource(serviceProvider));
        }
    }
سپس می‌توان این متد AddEFConfig را به صورت زیر به تنظیمات برنامه در کلاس Startup اضافه و معرفی کرد:
namespace DbConfig.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, MyAppContext>();
            services.AddScoped<IConfigurationValuesService, ConfigurationValuesService>();

            var connectionString = Configuration.GetConnectionString("SqlServerConnection")
                     .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
            services.AddDbContext<MyAppContext>(options =>
                    {
                        options.UseSqlServer(
                            connectionString,
                            dbOptions =>
                                {
                                    var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                                    dbOptions.CommandTimeout(minutes);
                                    dbOptions.EnableRetryOnFailure();
                                });
                    });

            var serviceProvider = services.BuildServiceProvider();
            var configuration = new ConfigurationBuilder()
                                       .AddConfiguration(Configuration) // Adds all of the existing configurations
                                       .AddEFConfig(serviceProvider)
                                       .Build();
            services.AddSingleton<IConfigurationRoot>(sp => configuration); // Replace
            services.AddSingleton<IConfiguration>(sp => configuration); // Replace
در اینجا ابتدا نیاز است یک ConfigurationBuilder جدید را ایجاد کنیم تا بتوان AddEFConfig را بر روی آن فراخوانی کرد. در این بین، خود برنامه نیز تعدادی تامین کننده‌ی تنظیمات پیش‌فرض را نیز دارد که قصد نداریم سبب پاک شدن آن‌ها شویم. به همین جهت آن‌ها را توسط متد AddConfiguration، افزوده‌ایم. پس از تعریف این ConfigurationBuilder جدید، نیاز است آن‌را جایگزین IConfiguration و IConfigurationRoot پیش‌فرض برنامه کنیم که روش آن‌را در دو متد services.AddSingleton ملاحظه می‌کنید.
همچنین روش دسترسی به serviceProvider مورد نیاز AddEFConfig، توسط متد services.BuildServiceProvider نیز در کدهای فوق مشخص است. به همین جهت مجبور شدیم این تعریف را در اینجا قرار دهیم و گرنه می‌شد از کلاس Program و یا حتی سازنده‌ی کلاس Startup نیز استفاده کرد. مشکل این دو مکان عدم دسترسی به سرویس IUnitOfWork و سایر تنظیمات برنامه است.


آزمایش برنامه

اگر به قسمت «ساختار Context برنامه و مقدار دهی اولیه‌ی آن» مطلب جاری دقت کرده باشید، دو کلید پیش‌فرض در اینجا ثبت شده‌اند. به همین جهت در ادامه با تزریق سرویس IConfiguration به سازنده‌ی یک کنترلر، سعی در خواندن مقادیر آن‌ها خواهیم کرد:
namespace DbConfig.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IConfiguration _configuration;

        public HomeController(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        public IActionResult Index()
        {
            return Json(
                new
                {
                    key1 = _configuration["key-1"],
                    key2 = _configuration["key-2"]
                });
        }
با این خروجی:



به روز رسانی بانک اطلاعاتی برنامه و بارگذاری مجدد اطلاعات IConfiguration

فرض کنید توسط سرویسی، اطلاعات جدول ConfigurationValue را تغییر داده‌اید. نکته‌ی مهم اینجا است که اینکار سبب فراخوانی مجدد متد Load کلاس EFConfigurationProvider نخواهد شد و عملا این تغییرات در سراسر برنامه توسط تزریق اینترفیس IConfiguration قابل دسترسی نخواهند بود (مگر اینکه برنامه مجددا ری‌استارت شود). نکته‌ی به روز رسانی این اطلاعات به صورت زیر است:
    public class ConfigurationValuesService : IConfigurationValuesService
    {
        private readonly IConfiguration _configuration;

        public ConfigurationValuesService(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        private void reloadEFConfigurationProvider()
        {
            ((IConfigurationRoot)_configuration).Reload();
        }
در جائیکه نیاز است پس از به روز رسانی بانک اطلاعاتی، تنظیمات برنامه را نیز بارگذاری مجدد کنید، ابتدا اینترفیس IConfiguration را به سازنده‌ی آن تزریق کرده و سپس به نحو فوق، متد Reload را فراخوانی کنید. اینکار سبب می‌شود تا یکبار دیگری متد Load کلاس EFConfigurationProvider نیز فراخوانی شود که باعث بارگذاری مجدد تنظیمات برنامه خواهد شد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreDbConfig.zip
مطالب
پیاده سازی مکانیسم سعی مجدد (Retry)
فرض کنید در برنامه‌ای که نوشته‌اید، قصد فراخونی یک وب سرویس را دارید. به طور قطع نمی‌توان همیشه انتظار داشت این سرویس مورد نظر بدون هیچ مشکلی اجرا شود و خروجی مورد نظر را بدهد. برای نمونه ممکن است در لحظه فراخوانی متد مورد نظر، اختلالی در شبکه رخ دهد و فراخوانی سرویس شما با مشکل مواجه شود. در چنین مواقعی دو مورد را پیش‌رو داریم: 
- یک: اعلام نتیجه عدم موفق بودن فراخوانی.
- دو: یک (یا چند) بار دیگر، سعی در فراخوانی سرویس مورد نظر کنیم.
مکانیسم سعی مجدد فقط و فقط محدود به فراخوانی وب سرویس‌ها نمی‌شود. برای نمونه می‌توان به ارسال ایمیل، خطا در اجرای یک کوئری T_SQL و مورادی از این قبیل اشاره کرد.
چرا باید سعی مجدد را پیاده سازی کنیم؟ 
عدم وجود امکان سعی مجدد هیچ چیزی را از پروژه شما سلب نمیکند؛ ولی وجود آن یک امکان را به پروژه شما اضافه میکند که تا حدودی باعث سهولت در استفاده از نرم افزار شما خواهد شد.
نباید‌های مکانیسم سعی مجدد
با خواندن مطالب فوق شاید به این موضوع فکر کنید که مکانیسم سعی مجدد امکان خوبی برای پروژه است و همه بخش‌های پروژه را درگیر این مکانیز کنیم. واقعیت این است که استفاده از مکانیسم سعی مجدد بهتر است محدود به بخش هایی از پروژه شود و نه کل پروژه.
پیش نیاز‌ها
تا به این مرحله با «مکانیسم سعی مجدد» بیشتر آشنا شدیم. برای پیاده سازی یک «سعی مجدد» نیازمندیم یک سری موارد را بدانیم:
یک: میزان تعداد دفعات تلاش 
دو: اختلاف بین هر دو  تلاش مجدد (وقفه)
سه: مقدار افزایش وقفه
چهار: سعی مجدد بر اساس نوع Exception

سه مورد اول از لیست  بالا تقریبا برای یک پیاده سازی سعی مجدد پاسخگو می‌باشد. در ادامه ابتدا قصد داریم یک «سعی مجدد ساده» را نوشته و سپس به معرفی یکی از کتابخانه‌های پرکاربرد آن می‌پردازیم.
قطعه کد زیر را در نظر بگیرد که شبیه ساز ارسال ایمیل می‌باشد:
 public class Mailer
    {
        public static bool SendEmail()
        {
            Console.WriteLine("Sending Mail ...");

            // simulate error
            Random rnd = new Random();
            var rndNumber = rnd.Next(1, 10);
            if (rndNumber != 3) // *
                throw new SmtpFailedRecipientException();

            Console.WriteLine("Mail Sent successfully");
            return true;
        }
    }
خط *  برای شبیه  سازی وقوع یک خطا استفاده شده است.
 قطعه کد زیر برای پیاده سازی مکانیزم سعی مجدد می‌باشد:
 public static class Retry
    {
        public static void Do(Action action,TimeSpan retryInterval,int maxAttemptCount = 3)
        {
            Do<object>(() =>
            {
                action();
                return null;
            }, retryInterval, maxAttemptCount);
        }

        public static T Do<T>(Func<T> action,TimeSpan retryInterval,int maxAttemptCount = 3)
        {
            var exceptions = new List<Exception>();

            for (int attempted = 0; attempted < maxAttemptCount; attempted++)
            {
                try
                {
                    if (attempted > 0)
                    {
                        Thread.Sleep(retryInterval);
                    }
                    return action();
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }
            throw new AggregateException(exceptions);
        }
    }
قطعه کد فوق ساده‌ترین حالت پیاده سازی Retry می‌باشد که به تعداد MaxAttemptCount سعی در فراخوانی متد مورد نظر خواهد کرد.
یادآوری: متد Do با پارامتر Action در پارامتر اول جهت توابعی که مقدار خروجی ندارند می‌باشد.
همانطور که ذکر شد مقدار Interval بهتر است طبق یک مقدار از پیش تعیین شده افزایش یابد تا درخواست‌های ما با بازه زمانی خیلی کوتاهی نسبت به هم اجرا نشوند. برای رفع این مشکل بعد از Sleep می‌توان مقدار Interval را به صورت زیر افزایش داد:
retryInterval= retryInterval.Add(TimeSpan.FromSeconds(10));
همانطور که بیان کردیم ، قطعه کد نوشته شده فوق برای انجام یک Retry بسیار ساده می‌باشد. موارد دیگری را می‌توان به Retry فوق اضافه نمود. برای نمونه اگر Exception رخ داده شده از نوع مورد نظر ما بود، مجدد Retry کند، در غیر اینصورت از ادامه کار منصرف شود. برای نوشتن هندل کردن نوع Exception می‌توانیم از کتابخانه Polly استفاده کنیم.

کتابخانه Polly
Polly یکی از کتابخانه‌های پرکاربرد است و یکی از امکانات آن، «مکانیسم سعی مجدد» آن، به صورت زیر می‌باشد:


در ساده‌ترین حالت، استفاده از Polly همانند زیر است:

var policy = Policy.Handle<SmtpFailedRecipientException>().Retry();
            policy.Execute(Mailer.SendEmail);

متد Retry، دارای Overload‌های مختلفی است که یکی از آنها مقدار تعداد دفعات تلاش را دریافت می‌کند؛ همانند:

var policy = Policy.Handle<SmtpFailedRecipientException>().Retry(5);

لازم به ذکر است که باید دقیقا Exception مورد نظر را در بخش Config به کار ببرید. برای نمونه اگر کد فوق را همانند زیر به کار ببرید، در صورتیکه متد ارسال ایمیل با خطایی مواجه شود، هیچ تلاشی برای اجرای مجدد نخواهد کرد:

   var policy = Policy.Handle<SqlException>().Retry(5);

برای نمونه می‌توان از متد ForEver آن استفاده کرد تا زمانیکه متد مورد نظر Success نشده باشد، سعی در اجرای آن کند:

Policy
  .Handle<DivideByZeroException>()
  .RetryForever()

جهت کسب اطلاعات بیشتر می‌توانید در مخزن کد آن با سایر قابلیت‌های کتابخانه Polly بیشتر آشنا شوید: Github