نظرات مطالب
- همان بحث «iTextSharp و استفاده از قلمهای محدود فارسی» هست. به همین جهت در این کتابخانه جائیکه قلمها معرفی میشوند، امکان ثبت دو قلم را دارید تا در پشت صحنه همان FontSelector را تشکیل دهد؛ یا .... از یک قلم کامل استفاده کنید.
- البته باید در نظر داشت که قسمت تولید PDF از HTML بر اساس XMLWorker، از قابلیت FontSelector استفاده نمیکند و تک قلمی بیشتر نیست (کتابخانهی XMLWorker به این صورت و بدون در نظر گرفتن ویژگی FontSelector طراحی شده).
نظرات مطالب
نحوه استفاده از ViewModel در ASP.NET MVC
- شما هم از ViewModel استفاده کردی و هم از ViewBag با هم در یک View. از یکی بهتره استفاده کنید. ترجیح هم با ViewModel است تا به خواص آن به صورت strongly typed در یک View دسترسی داشته باشید و تحت کنترل کامپایلر هم باشد (تمام ViewBagها رو حذف و تبدیل به خواص ViewModel کنید).
- کلاس ViewModel ایی که تدارک دیدید، دارای سازندهای است که باید مقدار دهی شود پیش از استفاده. خوب ... MVC از کجا باید تشخیص بده که در زمان Post اطلاعات به سرور چطور باید اینرو به صورت خودکار وهله سازی کنه؟ به عبارتی ViewModel شما باید بدون سازنده، قابل وهله سازی باشد. سازنده رو حذف و صرفا از خواص استفاده کنید.
- برای ثبت نهایی هم باید اطلاعات خواص ViewModel رو (اونهایی رو که در Model یا جدول خاص شما وجود دارند) به Model انتساب بدی. بعد این وهله Add شود به Context و دست آخر هم ذخیره.
(البته کار شما به نظر لایه بندی صحیحی نداره وگرنه جای این مسایل در لایه سرویس برنامه است؛ چه بازگشت ViewModel و چه ذخیره سازی اطلاعات آن)
- کلاس ViewModel ایی که تدارک دیدید، دارای سازندهای است که باید مقدار دهی شود پیش از استفاده. خوب ... MVC از کجا باید تشخیص بده که در زمان Post اطلاعات به سرور چطور باید اینرو به صورت خودکار وهله سازی کنه؟ به عبارتی ViewModel شما باید بدون سازنده، قابل وهله سازی باشد. سازنده رو حذف و صرفا از خواص استفاده کنید.
- برای ثبت نهایی هم باید اطلاعات خواص ViewModel رو (اونهایی رو که در Model یا جدول خاص شما وجود دارند) به Model انتساب بدی. بعد این وهله Add شود به Context و دست آخر هم ذخیره.
(البته کار شما به نظر لایه بندی صحیحی نداره وگرنه جای این مسایل در لایه سرویس برنامه است؛ چه بازگشت ViewModel و چه ذخیره سازی اطلاعات آن)
مسیرراهها
آشنایی با Refactoring
مقالات مرتبط
بعد از انتشار قسمت 6 به عنوان آخرین قسمت مرتبط با تفکر مبتنیبر CRUD (CRUD-based thinking) قصد دارم پشتیبانی از طراحی Application Layer مبتنیبر CQRS را نیز به این زیرساخت اضافه کنم.
در این مطلب تغییرات حاصل از طراحی مجدد و بازسازی انجام شده در نسخه جدید را مرور خواهیم کرد.
تغییرات کتابخانه DNTFrameworkCore
1- واسطهای مورد استفاده جهت ردیابی موجودیتها :
public interface ICreationTracking { DateTime CreatedDateTime { get; set; } } public interface IModificationTracking { DateTime? ModifiedDateTime { get; set; } }
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": "متأسفانه مشکلی در فرآیند انجام درخواست شما پیش آمده است!" }
_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); }
خصوصیات 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();
تغییرات کتابخانه 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)); } }
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); }
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); } }
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); } }
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)} }; });
تغییرات کتابخانه 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
هزاران کتابخانهی جاوا اسکریپتی مستقل از AngularJS 2.0 وجود دارند که نیاز است بتوانیم از آنها در برنامههای مختلفی استفاده کنیم. در این مطلب، دو روش بارگذاری آنها را بررسی خواهیم کرد.
هدف: استفاده از کتابخانهی jsSHA
میخواهیم در یک برنامهی AngularJS 2.0، از کتابخانهی jsSHA استفاده کرده و هش SHA512 یک رشته را محاسبه کنیم.
تامین پیشنیازهای اولیه
میتوان فایلهای این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است اینکار را توسط npm مدیریت کنیم. به همین جهت فایل package.json آنرا گشوده و سپس مدخل متناظری را به آن اضافه کنید:
به این ترتیب با اجرای دستور npm install بر روی پوشهی جاری و یا ذخیرهی فایل در ویژوال استودیو، کار دریافت خودکار این کتابخانه صورت گرفته و در مسیر node_modules\jssha\src ذخیره میشود.
بارگذاری فایلهای کتابخانه به صورت پویا
یک روش استفاده از این کتابخانه یا هر کتابخانهی جاوا اسکریپتی، افزودن مدخل تعریف آن به صفحهی index.html است:
این روش هر چند کار میکند، اما با توجه به اینکه AngularJS 2.0 از System.JS برای مدیریت ماژولهای خود کمک میگیرد، میتواند با روش پویای ذیل جایگزین شود. برای این منظور ابتدا فایل systemjs.config.js را باز کنید و سپس دو تغییر ذیل را به آن اعمال نمائید:
در اینجا به اشیاء map و packages آن فایل که کار بارگذاری ماژولها را به صورت خودکار انجام میدهد، تعاریف جدید jssha را اضافه کردهایم. در قسمت map، مسیر پوشهی فایلهای js این کتابخانه مشخص شدهاند و در قسمت packages، نام فایل اصلی مدنظر و پسوندهای آنها ذکر گردیدهاند.
به این ترتیب هر زمانیکه کار import این کتابخانه صورت گیرد، بارگذاری پویای آن انجام خواهد شد. به علاوه ابزارهای بسته بندی و deploy پروژه هم این فایل را پردازش کرده و به صورت خودکار، کار bundling، فشرده سازی و یکی سازی اسکریپتها را انجام میدهند.
استفاده از jsSHA به صورت untyped
پس از دریافت بستههای این کتابخانه و مشخص سازی نحوهی بارگذاری پویای آن، اکنون نوبت به استفادهی از آن است. در اینجا منظور از untyped این است که فرض کنیم برای این کتابخانه، فایلهای typings مخصوص TypeScript وجود ندارند و پس از جستجوی در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped نتوانستهایم معادلی را برای آن پیدا کنیم. بنابراین فایل جدید untyped-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
با این قالب untyped-sha.component.html
توضیحات
هر زمانیکه فایلهای typing یک کتابخانهی جاوا اسکریپتی در دسترس نبودند، فقط کافی است از روش declare var jsSHA: any استفاده کنید. در اینجا any به همان حالت استاندارد و بینوع جاوا اسکریپت اشاره میکند. در این حالت برنامه بدون مشکل کامپایل خواهد شد؛ اما از تمام مزایای TypeScript مانند بررسی نوعها و همچنین intellisense محروم میشویم.
در این مثال در hook ویژهای به نام OnInit، کار ساخت شیء SHA را انجام داده و سپس هش عبارت This is a test محاسبه شده و به خاصیت عمومی hash انتساب داده میشود. سپس این خاصیت عمومی، در قالب این کامپوننت از طریق روش interpolation نمایش داده شدهاست.
دو نکتهی مهم
الف) اگر از روش declare var jsSHA: any استفاده کردید، کار بارگذاری فایل sha512.js به صورت خودکار رخ نخواهد داد؛ چون ماژولی را import نمیکند. بنابراین تعاریف systemjs.config.js ندید گرفته خواهد شد. در این حالت باید از همان روش متداول افزودن تگ script این کتابخانه به فایل index.html استفاده کرد.
ب) برای بارگذاری پویای کتابخانهی jsSHA بر اساس تعاریف فایل systemjs.config.js از متد require کمک بگیرید:
در این حالت باز هم متغیر jsSHA تعریف شده از نوع any است؛ اما اینبار متد require کار بارگذاری خودکار ماژولی را به نام jssha، انجام میدهد. این بارگذاری هم بر اساس تعاریف قسمت «بارگذاری فایلهای کتابخانه به صورت پویا» ابتدای بحث کار میکند.
استفاده از jsSHA به صورت typed
کتابخانهی jsSHA در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jssha دارای فایل d.ts. مخصوص خود است. برای نصب آن از یکی از دو روش ذیل استفاده کنید:
الف) نصب دستی فایلهای typings
توسط خط فرمان، به پوشهی ریشهی پروژه وارد شده و دو دستور فوق را صادر کنید. به این ترتیب فایلهای d.ts. لازم، به پوشهی typings پروژه اضافه میشوند.
ب) تکمیل فایل typings.ts
برای این منظور فایل typings.json را گشوده و سپس سطر جدید فوق را به آن اضافه کنید. اکنون اگر فایل package.json را یکبار دیگر ذخیره کنید و یا دستور npm install را صادر کنید، همان مراحل قسمت الف تکرار خواهند شد.
پس از نصب فایلهای typings این پروژه، به فایل main.ts مراجعه کرده و مدخل ذیل را به ابتدای آن اضافه کنید:
اینکار سبب خواهد شد تا intellisense درون ویژوال استودیو بتواند مداخل متناظر را یافته و راهنمای مناسبی را ارائه دهد.
در ادامه فایل جدید typed-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
محتویات فایل typed-sha.component.html با محتویات فایل untyped-sha.component.html که پیشتر عنوان شد، یکی است.
در اینجا تنها نکتهی مهم و جدید نسبت به روش قبل (استفاده از jsSHA به صورت untyped)، روش import این کتابخانه است. روش "import * as jsSHA from "jssha به عبارت var jsSHA = require("jssha") ترجمه میشود که در نهایت سبب بارگذاری خودکار فایلهای jssha بر اساس تعاریف مداخل آن در فایل systemjs.config.js میگردد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید.
هدف: استفاده از کتابخانهی jsSHA
میخواهیم در یک برنامهی AngularJS 2.0، از کتابخانهی jsSHA استفاده کرده و هش SHA512 یک رشته را محاسبه کنیم.
تامین پیشنیازهای اولیه
میتوان فایلهای این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است اینکار را توسط npm مدیریت کنیم. به همین جهت فایل package.json آنرا گشوده و سپس مدخل متناظری را به آن اضافه کنید:
"dependencies": { // ... "jssha": "^2.1.0", // ... },
بارگذاری فایلهای کتابخانه به صورت پویا
یک روش استفاده از این کتابخانه یا هر کتابخانهی جاوا اسکریپتی، افزودن مدخل تعریف آن به صفحهی index.html است:
<script src="node_modules/jssha/src/sha512.js"></script>
// map tells the System loader where to look for things var map = { // ... 'jssha': 'node_modules/jssha/src' }; // packages tells the System loader how to load when no filename and/or no extension var packages = { // ... 'jssha': { main: 'sha512.js', defaultExtension: 'js' } };
به این ترتیب هر زمانیکه کار import این کتابخانه صورت گیرد، بارگذاری پویای آن انجام خواهد شد. به علاوه ابزارهای بسته بندی و deploy پروژه هم این فایل را پردازش کرده و به صورت خودکار، کار bundling، فشرده سازی و یکی سازی اسکریپتها را انجام میدهند.
استفاده از jsSHA به صورت untyped
پس از دریافت بستههای این کتابخانه و مشخص سازی نحوهی بارگذاری پویای آن، اکنون نوبت به استفادهی از آن است. در اینجا منظور از untyped این است که فرض کنیم برای این کتابخانه، فایلهای typings مخصوص TypeScript وجود ندارند و پس از جستجوی در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped نتوانستهایم معادلی را برای آن پیدا کنیم. بنابراین فایل جدید untyped-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core'; var jsSHA = require("jssha"); // ==> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions //declare var jsSHA: any; // ==> this requires adding <script src="node_modules/jssha/src/sha512.js"></script> to the first page manually. @Component({ templateUrl: 'app/using-third-party-libraries/untyped-sha.component.html' }) export class UnTypedShaComponent implements OnInit { hash: String; ngOnInit(): void { let shaObj = new jsSHA("SHA-512", "TEXT"); shaObj.update("This is a test"); this.hash = shaObj.getHash("HEX"); } }
<h1>SHA-512 Hash / UnTyped</h1> <p>String: This is a test</p> <p>HEX: {{hash}}</p>
هر زمانیکه فایلهای typing یک کتابخانهی جاوا اسکریپتی در دسترس نبودند، فقط کافی است از روش declare var jsSHA: any استفاده کنید. در اینجا any به همان حالت استاندارد و بینوع جاوا اسکریپت اشاره میکند. در این حالت برنامه بدون مشکل کامپایل خواهد شد؛ اما از تمام مزایای TypeScript مانند بررسی نوعها و همچنین intellisense محروم میشویم.
در این مثال در hook ویژهای به نام OnInit، کار ساخت شیء SHA را انجام داده و سپس هش عبارت This is a test محاسبه شده و به خاصیت عمومی hash انتساب داده میشود. سپس این خاصیت عمومی، در قالب این کامپوننت از طریق روش interpolation نمایش داده شدهاست.
دو نکتهی مهم
الف) اگر از روش declare var jsSHA: any استفاده کردید، کار بارگذاری فایل sha512.js به صورت خودکار رخ نخواهد داد؛ چون ماژولی را import نمیکند. بنابراین تعاریف systemjs.config.js ندید گرفته خواهد شد. در این حالت باید از همان روش متداول افزودن تگ script این کتابخانه به فایل index.html استفاده کرد.
ب) برای بارگذاری پویای کتابخانهی jsSHA بر اساس تعاریف فایل systemjs.config.js از متد require کمک بگیرید:
var jsSHA = require("jssha");
استفاده از jsSHA به صورت typed
کتابخانهی jsSHA در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jssha دارای فایل d.ts. مخصوص خود است. برای نصب آن از یکی از دو روش ذیل استفاده کنید:
الف) نصب دستی فایلهای typings
npm install -g typings typings install jssha --save --ambient
ب) تکمیل فایل typings.ts
{ "ambientDependencies": { // ... "jssha": "registry:dt/jssha#2.1.0+20160317120654" } }
پس از نصب فایلهای typings این پروژه، به فایل main.ts مراجعه کرده و مدخل ذیل را به ابتدای آن اضافه کنید:
/// <reference path="../typings/browser/ambient/jssha/index.d.ts" />
در ادامه فایل جدید typed-sha.component.ts را با محتوای ذیل به پروژه اضافه کنید:
import { Component, OnInit } from '@angular/core'; //import { jsSHA } from "jssha"; import * as jsSHA from "jssha"; // ===> var jsSHA = require("jssha"); // ===> loads `sha512.js` file dynamically using `systemjs.config.js` file definitions @Component({ templateUrl: 'app/using-third-party-libraries/typed-sha.component.html' }) export class TypedShaComponent implements OnInit{ hash: String; ngOnInit(): void { let shaObj = new jsSHA("SHA-512", "TEXT"); shaObj.update("This is a test"); this.hash = shaObj.getHash("HEX"); } }
در اینجا تنها نکتهی مهم و جدید نسبت به روش قبل (استفاده از jsSHA به صورت untyped)، روش import این کتابخانه است. روش "import * as jsSHA from "jssha به عبارت var jsSHA = require("jssha") ترجمه میشود که در نهایت سبب بارگذاری خودکار فایلهای jssha بر اساس تعاریف مداخل آن در فایل systemjs.config.js میگردد.
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید.
برای تهیه یک RadioButtonList نیز میتوان از همان نکتهی CheckBoxList استفاده کرد: نام عناصر radio button اضافه شده به صفحه را یکسان وارد میکنیم. به این ترتیب یک گروه تشکیل خواهد شد و زمانیکه اطلاعات این عناصر به سرور ارسال میشود، اینبار بجای یک آرایه، تنها مقدار کنترل انتخاب شده، ارسال میگردد. یک مثال:
یک پروژه جدید و خالی ASP.NET MVC را آغاز کنید. سپس کنترلر Home و View خالی Index را نیز ایجاد نمائید. محتویات این دو را به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm1 (Normal)</legend>
@using (Html.BeginForm(actionName: "HandleForm1", controllerName: "Home"))
{
@:your favorite tech: <br />
@Html.RadioButton(name: "tech", value: ".NET", isChecked: true) @:DOTNET <br />
@Html.RadioButton(name: "tech", value: "JAVA", isChecked: false) @:JAVA <br />
@Html.RadioButton(name: "tech", value: "PHP", isChecked: false) @:PHP <br />
<input type="submit" value="Submit" />
}
</fieldset>
using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult HandleForm1(string tech)
{
return RedirectToAction("Index");
}
}
}
در اینجا سه RadioButton با نامی یکسان در صفحه اضافه شدهاند. سپس داخل متد HandleForm1 یک breakpoint قرار دهید. اکنون برنامه را اجرا کنید و فرم را به سرور ارسال نمائید. پارامتر tech با value عنصر انتخابی مقدار دهی خواهد شد.
تهیه یک RadioButtonList عمومی
اطلاعات فوق را میتوان تبدیل به یک HtmlHelper با قابلیت استفاده مجدد نیز نمود:
@helper RadioButtonList(string groupName, IEnumerable<System.Web.Mvc.SelectListItem> items)
{
<div class="RadioButtonList">
@foreach (var item in items)
{
@item.Text
<input type="radio" name="@groupName"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
<br />
}
</div>
}
برای مثال یک فایل را در مسیر app_code\Helpers.cshtml ایجاد کرده و اطلاعات فوق را به آن اضافه نمائید.
اینبار برای استفاده از آن خواهیم داشت:
using System.Collections.Generic;
using System.Web.Mvc;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new[]
{
new SelectListItem { Text = ".NET", Value = "Val1", Selected = true },
new SelectListItem { Text = "JAVA", Value = "Val2", Selected = false },
new SelectListItem { Text = "PHP", Value = "Val3", Selected = false }
};
return View();
}
[HttpPost]
public ActionResult HandleForm2(string preferredTechnology)
{
return RedirectToAction("Index");
}
}
}
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm2 (Helper)</legend>
@using (Html.BeginForm(actionName: "HandleForm2", controllerName: "Home"))
{
@:your favorite tech: <br />
@Helpers.RadioButtonList("preferredTechnology", (SelectListItem[])ViewBag.Tags)
<input type="submit" value="Submit" />
}
</fieldset>
متد سفارشی تهیه شده، یک آرایه از SelectListItem ها را دریافت کرده و به صورت خودکار تبدیل به RadioButtonList میکند. بر اساس نام آن میتوان به مقدار انتخاب شده ارسالی به سرور در کنترلر مرتبط، دسترسی یافت.
تهیه یک Templated helper سفارشی
در عمل زمانیکه با مدلها کار میکنیم و اطلاعات برنامه قرار است Strongly typed باشند، مرسوم است لیستی از انتخابها را به صورت یک enum تعریف کنند. برای مثال مدل زیر را به برنامه اضافه کنید:
using System.ComponentModel.DataAnnotations;
namespace MvcApplication23.Models
{
public enum Gender
{
[Display(Name = "مرد")]
Male,
[Display(Name = "زن")]
Female,
}
public class User
{
[ScaffoldColumn(false)]
public int Id { set; get; }
[Display(Name = "نام")]
public string Name { set; get; }
[Display(Name = "جنسیت")]
[UIHint("EnumRadioButtonList")]
public Gender Gender { set; get; }
}
}
قصد داریم یک Templated helper سفارشی را به نام EnumRadioButtonList، ایجاد کنیم تا در زمان فراخوانی متد Html.EditorForModel، به صورت خودکار enum تعریف شده را به صورت یک RadioButtonList نمایش دهد.
برای این منظور فایل جدید Views\Shared\EditorTemplates\EnumRadioButtonList.cshtml را به برنامه اضافه کنید. محتوای آنرا به نحو زیر تغییر دهید:
@using System.ComponentModel.DataAnnotations
@using System.Globalization
@model Enum
@{
Func<Enum, string> getDescription = enumItem =>
{
var type = enumItem.GetType();
var memInfo = type.GetMember(enumItem.ToString());
if (memInfo != null && memInfo.Any())
{
var attrs = memInfo[0].GetCustomAttributes(typeof(DisplayAttribute), false);
if (attrs != null && attrs.Any())
return ((DisplayAttribute)attrs[0]).GetName();
}
return enumItem.ToString();
};
var listItems = Enum.GetValues(Model.GetType())
.OfType<Enum>()
.Select(enumItem =>
new SelectListItem()
{
Text = getDescription(enumItem),
Value = enumItem.ToString(),
Selected = enumItem.Equals(Model)
});
string prefix = ViewData.TemplateInfo.HtmlFieldPrefix;
ViewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
int index = 0;
foreach (var li in listItems)
{
string fieldName = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", prefix, index++);
<div class="editor-radio">
@Html.RadioButton(prefix, li.Value, li.Selected, new { @id = fieldName })
@Html.Label(fieldName, li.Text)
</div>
}
ViewData.TemplateInfo.HtmlFieldPrefix = prefix;
}
در اینجا به کمک Reflection به اطلاعات enum دریافتی دسترسی خواهیم داشت. بر این اساس میتوان نام عناصر آنرا یافت و تبدیل به یک RadioButtonList کرد. البته کار به همینجا ختم نمیشود. در این بین باید دقت داشت که ممکن است از ویژگی Display (مانند مدل نمونه فوق) بر روی تک تک عناصر یک enum نیز استفاده شود. به همین جهت این مورد نیز باید پردازش گردد.
نهایتا برای استفاده از این Templated helper سفارشی، کنترلر و View برنامه را به نحو زیر میتوان تغییر داد:
using System.Collections.Generic;
using System.Web.Mvc;
using MvcApplication23.Models;
namespace MvcApplication23.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
var user = new User { Id = 1, Name = "name 1", Gender = Gender.Male };
return View(user);
}
[HttpPost]
public ActionResult HandleForm3(User user)
{
return RedirectToAction("Index");
}
}
}
@model MvcApplication23.Models.User
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>HandleForm3 (EditorForModel)</legend>
@using (Html.BeginForm(actionName: "HandleForm3", controllerName: "Home"))
{
@Html.EditorForModel()
<input type="submit" value="Submit" />
}
</fieldset>
برای استفاده از یک templated helper سفارشی چندین روش وجود دارد:
الف) همانند مثال فوق از ویژگی UIHint استفاده شود.
ب) نام فایل را به enum.cshtml تغییر دهیم. به این ترتیب از این پس کلیه enumها در صورت استفاده از متد Html.EditorForModel، به صورت خودکار تبدیل به یک RadioButtonList میشوند.
ج) متد زیر نیز همین کار را انجام میدهد:
@Html.EditorFor(model => model.EnumProperty, "EnumRadioButtonList")
هر برنامهی وب، دارای یک frontend و یک backend است. تا اینجا، تمام تمرکز این سری، بر روی پیاده سازی frontend بود و هیچکدام از برنامههایی را که تکمیل کردیم، تبادل اطلاعاتی را با وب سرویسهای backend نداشتند؛ اما به عنوان یک توسعه دهندهی React، نیاز است با نحوهی ارتباط با سرور آشنایی داشت که در طی چند قسمت به آن میپردازیم.
ایجاد برنامهی backend ارائه دهندهی REST API
در اینجا یک برنامهی سادهی ASP.NET Core Web API را جهت تدارک سرویسهای backend، مورد استفاده قرار میدهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی میتوانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائهی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. میتوان به این endpintها درخواستهای HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته میشود که جهت آزمایش برنامهها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، میتوان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائهی آرایهای از اشیاء جاوا اسکریپتی قابل استفادهی در برنامههای frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال میکنیم، آرایهای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کردهاست.
اگر علاقمندید بودید میتوانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائهی مطالب آنرا با ASP.NET Core Web API نیز پیاده سازی میکنیم (برای مطالعهی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز میتوان استفاده کرد):
مدل مطالب
ساختار این مدل، با ساختار مدل مطالب JSONPlaceholder یکی درنظر گرفته شدهاست، تا مطلب قابلیت پیگیری بیشتری را پیدا کند.
منبع دادهی فرضی مطالب
برای ارائهی سادهتر برنامه، یک منبع دادهی درون حافظهای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار میدهیم:
در این سرویس، نیازمندیهای کنترلر مطالب مانند ارائه لیست تمام مطالب، نمایش اطلاعات یک مطلب، به روز رسانی، ایجاد و حذف یک مطلب، تدارک دیده شدهاند. سپس از این سرویس در کنترلر زیر استفاده میکنیم:
کنترلر Web API برنامهی backend
این کنترلر که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست که در ادامه، در برنامهی React خود از آنها استفاده خواهیم کرد. پس از ایجاد این پروژهی web api، یک نمونه خروجی آن در مسیر https://localhost:5001/api/posts، به صورت زیر خواهد بود:
البته نمایش فرمت شدهی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.
ایجاد ساختار ابتدایی برنامهی ارتباط با سرور
در اینجا برای بررسی کار با سرور، یک پروژهی جدید React را ایجاد میکنیم:
در ادامه توئیتر بوت استرپ 4 را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
سپس برای افزودن فایل bootstrap.css به پروژهی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
سپس فایل app.js را به شکل زیر تکمیل میکنیم:
که حاصل آن، یک دکمه، برای افزودن مطلبی جدید، به همراه جدولی است از مطالب که قصد داریم در ادامه، اطلاعات آنرا از سرور دریافت کرده و حذف و یا به روز رسانی کنیم:
نگاهی به انواع و اقسام HTTP Clientهای مهیا
در ادامه نیاز خواهیم داشت تا از طریق برنامههای React خود، درخواستهای HTTP را به سمت سرور (یا همان برنامهی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راهحل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت میتوانید از کتابخانههایی که خودتان ترجیح میدهید، نسبت به کتابخانههایی که به شما ارائه/تحمیل (!) میشوند (مانند برنامههای Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شدهاست! ابتدا با یک کتابخانهی HTTP مقدماتی شروع کردند. بعدی آنرا منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابستهاست به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار میکنید، باید کارها را آنگونه که Angular میپسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح میشود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمیتر آنها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید میتوانید از محصور کنندههای آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی میتوانید از آن در برنامههای React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانهی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژهی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!
نصب Axios در برنامهی React این قسمت
برای نصب کتابخانهی Axios، در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
پس از برپایی این مقدمات، ادامهی مطلب «ارتباط با سرور» را در قسمت بعدی پیگیری میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
ایجاد برنامهی backend ارائه دهندهی REST API
در اینجا یک برنامهی سادهی ASP.NET Core Web API را جهت تدارک سرویسهای backend، مورد استفاده قرار میدهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی میتوانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائهی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. میتوان به این endpintها درخواستهای HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته میشود که جهت آزمایش برنامهها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، میتوان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائهی آرایهای از اشیاء جاوا اسکریپتی قابل استفادهی در برنامههای frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال میکنیم، آرایهای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کردهاست.
اگر علاقمندید بودید میتوانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائهی مطالب آنرا با ASP.NET Core Web API نیز پیاده سازی میکنیم (برای مطالعهی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز میتوان استفاده کرد):
مدل مطالب
namespace sample_22_backend.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } public int UserId { set; get; } } }
منبع دادهی فرضی مطالب
برای ارائهی سادهتر برنامه، یک منبع دادهی درون حافظهای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار میدهیم:
using System; using System.Collections.Generic; using System.Linq; using sample_22_backend.Models; namespace sample_22_backend.Services { public interface IPostsDataSource { List<Post> GetAllPosts(); bool DeletePost(int id); Post AddPost(Post post); bool UpdatePost(int id, Post post); Post GetPost(int id); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> public class PostsDataSource : IPostsDataSource { private readonly List<Post> _allPosts; public PostsDataSource() { _allPosts = createDataSource(); } public List<Post> GetAllPosts() { return _allPosts; } public Post GetPost(int id) { return _allPosts.Find(x => x.Id == id); } public bool DeletePost(int id) { var item = _allPosts.Find(x => x.Id == id); if (item == null) { return false; } _allPosts.Remove(item); return true; } public Post AddPost(Post post) { var id = 1; var lastItem = _allPosts.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } post.Id = id; _allPosts.Add(post); return post; } public bool UpdatePost(int id, Post post) { var item = _allPosts .Select((pst, index) => new { Item = pst, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null || id != post.Id) { return false; } _allPosts[item.Index] = post; return true; } private static List<Post> createDataSource() { var list = new List<Post>(); var rnd = new Random(); for (var i = 1; i < 10; i++) { list.Add(new Post { Id = i, UserId = rnd.Next(1, 1000), Title = $"Title {i} ...", Body = $"Body {i} ..." }); } return list; } } }
کنترلر Web API برنامهی backend
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using sample_22_backend.Models; using sample_22_backend.Services; namespace sample_22_backend.Controllers { [ApiController] [Route("api/[controller]")] public class PostsController : ControllerBase { private readonly IPostsDataSource _postsDataSource; public PostsController(IPostsDataSource postsDataSource) { _postsDataSource = postsDataSource; } [HttpGet] public ActionResult<List<Post>> GetPosts() { return _postsDataSource.GetAllPosts(); } [HttpGet("{id}")] public ActionResult<Post> GetPost(int id) { var post = _postsDataSource.GetPost(id); if (post == null) { return NotFound(); } return Ok(post); } [HttpDelete("{id}")] public ActionResult DeletePost(int id) { var deleted = _postsDataSource.DeletePost(id); if (deleted) { return Ok(); } return NotFound(); } [HttpPost] public ActionResult<Post> CreatePost([FromBody]Post post) { post = _postsDataSource.AddPost(post); return CreatedAtRoute(nameof(GetPost), new { post.Id }, post); } [HttpPut("{id}")] public ActionResult<Post> UpdatePost(int id, [FromBody]Post post) { var updated = _postsDataSource.UpdatePost(id, post); if (updated) { return Ok(post); } return NotFound(); } } }
البته نمایش فرمت شدهی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.
ایجاد ساختار ابتدایی برنامهی ارتباط با سرور
در اینجا برای بررسی کار با سرور، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app sample-22-frontend > cd sample-22-frontend > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
سپس فایل app.js را به شکل زیر تکمیل میکنیم:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { posts: [] }; handleAdd = () => { console.log("Add"); }; handleUpdate = post => { console.log("Update", post); }; handleDelete = post => { console.log("Delete", post); }; render() { return ( <React.Fragment> <button className="btn btn-primary mt-1 mb-1" onClick={this.handleAdd}> Add </button> <table className="table"> <thead> <tr> <th>Title</th> <th>Update</th> <th>Delete</th> </tr> </thead> <tbody> {this.state.posts.map(post => ( <tr key={post.id}> <td>{post.title}</td> <td> <button className="btn btn-info btn-sm" onClick={() => this.handleUpdate(post)} > Update </button> </td> <td> <button className="btn btn-danger btn-sm" onClick={() => this.handleDelete(post)} > Delete </button> </td> </tr> ))} </tbody> </table> </React.Fragment> ); } } export default App;
نگاهی به انواع و اقسام HTTP Clientهای مهیا
در ادامه نیاز خواهیم داشت تا از طریق برنامههای React خود، درخواستهای HTTP را به سمت سرور (یا همان برنامهی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راهحل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت میتوانید از کتابخانههایی که خودتان ترجیح میدهید، نسبت به کتابخانههایی که به شما ارائه/تحمیل (!) میشوند (مانند برنامههای Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شدهاست! ابتدا با یک کتابخانهی HTTP مقدماتی شروع کردند. بعدی آنرا منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابستهاست به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار میکنید، باید کارها را آنگونه که Angular میپسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح میشود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمیتر آنها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید میتوانید از محصور کنندههای آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی میتوانید از آن در برنامههای React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانهی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژهی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!
نصب Axios در برنامهی React این قسمت
برای نصب کتابخانهی Axios، در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
یکی از مهمترین تغییرات دات نت 6، ارائهی Minimal API's به همراه آن است که نسبت به MVC و سایر مشتقات ASP.NET Core، کمتر به همراه پیشفرضهای نظری خاص و بسیار مقید و متعصبانه (opinionated) است؛ که این مورد خود مزیتی است جهت انجام امور متداول، به نحوی دیگر و دلخواه و با آزادی عمل بیشتری در طراحی endpoints مورد نیاز و کل برنامه. خصوصا این سبک جدید، با معماری برشهای عمودی (vertical slices) ارائه شدهی توسط نویسندهی AutoMapper، هماهنگی خاصی دارد و اینطور به نظر میرسد که جهت ساده سازی طراحی برنامههای ASP.NET Core با معماری CQRS ارائه شدهاست. با وجود Minimal API's میتوان از دو لایهی متداول برنامهها رها شد: لایهی سرویسها و لایهی مخازن یا Repositories. در معماری برشهای عمودی، برنامه به ویژگیهای خاصی (Features) تقسیم شده و هر ویژگی، متکی به خود طراحی میشود. زمانیکه از هندلرها برای هر Command و Query معماری CQRS استفاده میکنیم، اینها مختص به یک ویژگی متکی به خود طراحی میشوند و به همراه تمام اطلاعات و اعمال مرتبط هستند و دیگر در این حالت، وجود لایههای سرویس و مخزن، بیمعنا و غیرضروری میشوند.
در ادامه قصد داریم تمام این موارد را مانند Minimal API's و معماری برشهای عمودی به همراه CQRS، در طی یک سری و یک پروژهی عملی ساخت یک Blog به نام MinimalBlog، بررسی کنیم. البته هدف ما در اینجا صرفا ساخت backend ساختار یافتهی این برنامهاست؛ منهای UI آن. هدف اصلی ما از این سری، ارائهی یک معماری، جهت کار با Minimal API's است.
دریافت کدهای کامل این سری
جهت مرور سریعتر و سادهتر این سری، کدهای کامل آنرا از اینجا میتوانید دریافت کنید: MinimalBlog.zip
پروژههایی که برنامهی MinimalBlog را تشکیل میدهند
برنامهی MinimalBlog، تنها از سه پروژهی زیر تشکیل میشود:
MinimalBlog.Api: این پروژه از نوع minimal API's است که توسط دستور جدید «dotnet new webapi --use-minimal-apis» آغاز خواهد شد و به صورت پیشفرض به همراه پشتیبانی از OpenAPI نیز هست. البته اگر از VS2022 استفاده میکنید، در حین آغاز یک پروژهی Web API جدید، تیک مربوط به use controllers را در UI بردارید تا از Minimal API's استفاده شود.
MinimalBlog.Dal: که Dal در اینجا مخفف data access layer است و یک class library میباشد و با دستور dotnet new classlib آغاز میشود.
MinimalBlog.Domain: نیز یک class library است و با دستور dotnet new classlib آغاز میشود.
همانطور که مشاهده میکنید، این طراحی جدید، بدون وجود لایهی متداول سرویسها و یا مخازن است.
بررسی ساختار ابتدایی پروژهی MinimalBlog.Api
در اینجا تنها تک فایل Program.cs، به همراه تنظیمات برنامه قابل مشاهدهاست و فایل Starup.cs از آن حذف شدهاست (اطلاعات بیشتر). این فایل نیز بر مبنای مفهوم top level programs طراحی شدهاست و به همراه تعریف class و یا فضای نامی نیست:
همانطور که ملاحظه میکنید، تمام اتفاقات در همین تک فایل رخ میدهند. برای مثال سرویسهای مورد نیاز برنامه به مجموعهی builder.Services اضافه میشوند؛ شبیه به کاری که پیشتر در فایل Startup.cs و متد ConfigureServices آن انجام میدادیم.
پس از آن به تعاریف زیر میرسیم؛ تعاریف میان افزارهایی که پیشتر در متد Configure کلاس Startup انجام میشدند، الان همگی در تک فایل Program.cs قرار دارند:
البته هنوز هم میتوان در صورت نیاز به همان ساختار کلاس Startup پیشین نیز رسید.
در انتهای این فایل نیز تعاریف پیشفرض زیر قرار دارند:
در اینجا متد متد MapGet یک endpoint را تعریف کرده و سپس اکشنی را به آن انتساب میدهد. یعنی اگر آدرس weatherforecast/ درخواست شود، lambda expression تعریف شده، اجرا میشود. هدف از ارائهی Minimal API نیز همین است تا بتوان با حداقل کدنویسی، سریعا به نتیجهی مدنظر خود رسید.
در همین حال اگر برنامهی Api را اجرا کنیم، به تصویر زیر خواهیم رسید:
در ادامه کدهای موجود در این فایل را Refactor کرده و به کلاسهای دیگری منتقل میکنیم؛ چون اگر قرار باشد در طول زمان تمام endpoints مدنظر را در همینجا تعریف کنیم، کنترل برنامه از دست خارج خواهد شد.
غنی سازی Solution و کامپایلر #C با استفاده از فایلهای editorconfig. و Directory.Build.props
در مورد این دو فایل در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها » بیشتر بحث شدهاست. هدف از آنها، اعمال یکسری تنظیمات سراسری، به تمام پروژههای یک solution به صورت یکدست است؛ مانند تنظیمات کامپایلر جهت نمایش اخطارها به صورت خطاها، تعریف usingهای سراسری سیشارپ 10 و یا اعمال Roslyn analyzers به تمام پروژهها. این دو فایل را به همراه پروژهی پیوست میتوانید دریافت کنید و ... باید جزء استاندارد تمام پروژههای جدید باشند. چون وجود آنها سبب خواهد شد که به شدت کیفیت کدهای نهایی افزایش یابند و مبتنی بر یکسری best practices شوند.
در ادامه قصد داریم تمام این موارد را مانند Minimal API's و معماری برشهای عمودی به همراه CQRS، در طی یک سری و یک پروژهی عملی ساخت یک Blog به نام MinimalBlog، بررسی کنیم. البته هدف ما در اینجا صرفا ساخت backend ساختار یافتهی این برنامهاست؛ منهای UI آن. هدف اصلی ما از این سری، ارائهی یک معماری، جهت کار با Minimal API's است.
دریافت کدهای کامل این سری
جهت مرور سریعتر و سادهتر این سری، کدهای کامل آنرا از اینجا میتوانید دریافت کنید: MinimalBlog.zip
پروژههایی که برنامهی MinimalBlog را تشکیل میدهند
برنامهی MinimalBlog، تنها از سه پروژهی زیر تشکیل میشود:
MinimalBlog.Api: این پروژه از نوع minimal API's است که توسط دستور جدید «dotnet new webapi --use-minimal-apis» آغاز خواهد شد و به صورت پیشفرض به همراه پشتیبانی از OpenAPI نیز هست. البته اگر از VS2022 استفاده میکنید، در حین آغاز یک پروژهی Web API جدید، تیک مربوط به use controllers را در UI بردارید تا از Minimal API's استفاده شود.
MinimalBlog.Dal: که Dal در اینجا مخفف data access layer است و یک class library میباشد و با دستور dotnet new classlib آغاز میشود.
MinimalBlog.Domain: نیز یک class library است و با دستور dotnet new classlib آغاز میشود.
همانطور که مشاهده میکنید، این طراحی جدید، بدون وجود لایهی متداول سرویسها و یا مخازن است.
بررسی ساختار ابتدایی پروژهی MinimalBlog.Api
در اینجا تنها تک فایل Program.cs، به همراه تنظیمات برنامه قابل مشاهدهاست و فایل Starup.cs از آن حذف شدهاست (اطلاعات بیشتر). این فایل نیز بر مبنای مفهوم top level programs طراحی شدهاست و به همراه تعریف class و یا فضای نامی نیست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();
پس از آن به تعاریف زیر میرسیم؛ تعاریف میان افزارهایی که پیشتر در متد Configure کلاس Startup انجام میشدند، الان همگی در تک فایل Program.cs قرار دارند:
var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection();
در انتهای این فایل نیز تعاریف پیشفرض زیر قرار دارند:
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.Run(); record WeatherForecast(DateTime Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
در همین حال اگر برنامهی Api را اجرا کنیم، به تصویر زیر خواهیم رسید:
در ادامه کدهای موجود در این فایل را Refactor کرده و به کلاسهای دیگری منتقل میکنیم؛ چون اگر قرار باشد در طول زمان تمام endpoints مدنظر را در همینجا تعریف کنیم، کنترل برنامه از دست خارج خواهد شد.
غنی سازی Solution و کامپایلر #C با استفاده از فایلهای editorconfig. و Directory.Build.props
در مورد این دو فایل در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها » بیشتر بحث شدهاست. هدف از آنها، اعمال یکسری تنظیمات سراسری، به تمام پروژههای یک solution به صورت یکدست است؛ مانند تنظیمات کامپایلر جهت نمایش اخطارها به صورت خطاها، تعریف usingهای سراسری سیشارپ 10 و یا اعمال Roslyn analyzers به تمام پروژهها. این دو فایل را به همراه پروژهی پیوست میتوانید دریافت کنید و ... باید جزء استاندارد تمام پروژههای جدید باشند. چون وجود آنها سبب خواهد شد که به شدت کیفیت کدهای نهایی افزایش یابند و مبتنی بر یکسری best practices شوند.
مدتی هست که در حال تهیه کتابخانهی گزارش سازی مبتنی بر iTextSharp هستم و عمدهی استفاده از آن برای من تاکنون، تهیه گزارشات باکیفیت PDF فارسی تحت وب بوده؛ هر چند این کتابخانه وابستگی خاصی به فناوری مورد استفاده ندارد و با WinForms، WPF، مشتقات ASP.NET ، سرویسهای WCF و کلا هرجایی که دات نت فریم ورک کامل در دسترس باشد، قابل استفاده است. همچنین به منبع داده خاصی گره نخورده است و حتی میتوانید از یک anonymously typed list عدم متصل به بانک اطلاعاتی خاصی نیز به عنوان منبع داده آن استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عاملها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار میگیرد و جزئی از آن میشود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانههای معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاریهای مرتبط با هر کدام از کتابخانههای یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحهای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آنها اشارهای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمنهای برنامه نویسی مطرح میکنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آنها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.
اما چرا PDF؟
استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده میشود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینهای میتوان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونتهای شکیلتر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را میتوان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عاملها پشتیبانی میشود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عاملها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت میکند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامههای رایگان برای مشاهده فایلهای PDF زیاد است.
- تمام صفحات گزارش را در یک فایل میتوان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمیشود مثلا یک گزارش 100 صفحهای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست میدهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آنرا چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه میشود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتابهای الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتابها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین میتوان به فایلهای PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامههای PDF خوان معتبر گزارش داده شده و میتوان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایلها و تصاویر قرار داده شده در آن پشتیبانی میکند.
- از گرافیک برداری پشتیبانی میکند.
مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژهای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده میکند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایلهای اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده میکنید، محدودیتی را برای شما ایجاد نخواهد کرد.
کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره میتوانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.
نحوه تهیه اولین گزارش، با کتابخانه PdfReport
الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژههای VS.NET قابل استفاده است، میتوان از این پروژه Class library به عنوان کلاسهای پایه قابل استفاده در انواع و اقسام پروژههای مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.
ب) آخرین نگارش فایلهای مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلیهای موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن میباشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.
ج) کلاسهای زیر را به آن اضافه کنید:
از این کلاس برای مشخص سازی محل ذخیره سازی فایلهای نهایی PDF تولیدی استفاده خواهیم کرد.
همانطور که مشاهده میکنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.
در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظهای تعریف خواهیم کرد:
اکنون کلاس اصلی گزارش ما به صورت زیر خواهد بود:
و برای استفاده از آن:
برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثالهای همراه سورس پروژه در مخزن کد آن مراجعه نمائید.
توضیحات بیشتر:
- در قسمت DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم میشوند.
- سپس نیاز است قلمهای مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار میگیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص میشود. در مورد ساخت Footer سفارشی در قسمتهای بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمتهای بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیفهای گزارش مشخص میشود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالبهای سفارشی به مرور در قسمتهای بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین میشود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستونهای گزارش به کمک متد ColumnsWidthsType مشخص میشود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص میشود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار میگیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمتهای بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسبهای جمعهای پایین صفحات مشخص میشود.
- در قسمت MainTableColumns، ستونهایی را که علاقمندیم در گزارش ظاهر شوند، قید میکنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده میکنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستونهای آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، میتوان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، میتوان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجیهای دلخواه مورد نظر را میتوان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آنها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.
لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.
کتابخانه PdfReport به عمد جهت دات نت 3.5 تهیه شده است تا بازه وسیعی از سیستم عاملها را پوشش دهد.
این کتابخانه علاوه بر تبدیل اطلاعات شما به گزارشات مبتنی بر PDF، امکان تهیه خروجی خودکار اکسل (2007 به بعد) را نیز دارد. فایل خروجی آن، به صورت پیوست درون فایل PDF تهیه شده قرار میگیرد و جزئی از آن میشود.
بدیهی است اینبار با کتابخانه گزارش سازی روبرو هستید که با راست به چپ مشکلی ندارد!
کتابخانه PdfReport بر پایه کتابخانههای معروف سورس باز iTextSharp و EPPlus تهیه شده است. حداقل مزیت استفاده از آن، صرفه جویی در وقت شما جهت آموختن ریزه کاریهای مرتبط با هر کدام از کتابخانههای یاده شده است. برای نمونه جهت فراگیری کار با iTextSharp نیاز است یک کتاب 600 صفحهای به نام iText in action را مطالعه و تمرین کنید. این مورد منهای مسایل و نکات متعدد مرتبط با زبان فارسی است که در این کتاب به آنها اشارهای نشده است.
برای تهیه آن، مشکلات متداولی که کاربران مدام در انجمنهای برنامه نویسی مطرح میکنند و ابزارهای موجود عاجز از ارائه راه حلی ساده برای حل آنها هستند، مد نظر بوده و امید است نگارش یک این کتابخانه بتواند بسیاری از این دردها را کاهش دهد.
کار با این کتابخانه صرفا با کدنویسی میسر است (code first) و همین مساله انعطاف پذیری قابل توجهی را ایجاد کرده که در طی روزهای آینده با جزئیات بیشتر آن آشنا خواهید شد.
اما چرا PDF؟
استفاده از قالب PDF برای تهیه گزارشات، این مزایا را به همراه دارد:
- دقیقا همان چیزی که مشاهده میشود، در هر مکانی قابل چاپ است. با همان کیفیت، همان اندازه صفحه، همان فونت و غیره. به این ترتیب به صفحه بندی بسیار مناسب و بهینهای میتوان رسید و مشکلات گزارشات HTML ایی وب را ندارد.
- امکان استفاد از فونتهای شکیلتر در آن بدون مشکل و بدون نیاز به نصب بر روی کامپیوتری میسر است؛ چون فونت را میتوان در فایل PDF نیز قرار داد.
- این فایل در تمام سیستم عاملها پشتیبانی میشود. خصوصا اینکه فایل نهایی در تمام کامپیوترها و در انواع و اقسام سیستم عاملها به یک شکل و اندازه نمایش داده خواهد شد. برای مثال اینطور نیست که در ویندوز XP ،اعداد آن فارسی نمایش داده شوند و در ویندوز 7 با تنظیمات محلی خاصی، دیگر اینطور نباشد. حتی تحت لینوکس هم اعداد آن فارسی نمایش داده خواهد شد چون فونت مخصوص بکار رفته، در خود فایل PDF قابل قرار دادن است.
- برنامه معروف و رایگان Adobe reader برای خواندن و مشاهده آن کفایت میکند و البته کلاینت یکبار باید این برنامه را نصب کند. همچنین از این نوع برنامههای رایگان برای مشاهده فایلهای PDF زیاد است.
- تمام صفحات گزارش را در یک فایل میتوان داشت و به یکباره تمام آن نیز به سادگی قابل چاپ است. این مشکلی است که با گزارشات تحت وب وجود دارد که نمیشود مثلا یک گزارش 100 صفحهای را به یکباره به چاپگر ارسال کرد. به همین جهت عموما کاربران درخواست میدهند تا کل گزارش را در یک صفحه HTML نمایش دهید تا ما راحت آنرا چاپ کنیم یا راحت از آن خروجی بگیریم. اما زمانیکه فایل PDF تهیه میشود این مشکلات وجود نخواهد داشت و جهت Print بسیار بهینه سازی شده است. تا حدی که الان فرمت برگزیده تهیه کتابهای الکترونیکی نیز PDF است. مثلا سایت معروف آمازون امکان فروش نسخه PDF کتابها را هم پیش بینی کرده است.
-امکان صفحه بندی دقیق به همراه مشخص سازی landscape یا portrait بودن صفحه نهایی میسر است. چیزی که در گزارشات صفحات وب آنچنان معنایی ندارد.
- امکان رمزنگاری اطلاعات در آن پیش بینی شده است. همچنین میتوان به فایلهای PDF امضای دیجیتال نیز اضافه کرد. به این ترتیب هرگونه تغییری در محتوای فایل توسط برنامههای PDF خوان معتبر گزارش داده شده و میتوان از صحت اطلاعات ارائه شده توسط آن اطمینان حاصل کرد.
- از فشرده سازی مطالب، فایلها و تصاویر قرار داده شده در آن پشتیبانی میکند.
- از گرافیک برداری پشتیبانی میکند.
مجوز استفاده از این کتابخانه:
کار من مبتنی بر LGPL است. به این معنا که به صورت باینری (فایل dll) در هر نوع پروژهای قابل استفاده است.
اما ... PdfReport از دو کتابخانه دیگر نیز استفاده میکند:
- کتابخانه iTextSharp که دارای مجوز AGPL است. این مجوز رایگان نیست.
- کتابخانه EPPlus برای تولید فایلهای اکسل با کیفیت. مجوز استفاده از این کتابخانه LGPL است و تا زمانیکه به صورت باینری از آن استفاده میکنید، محدودیتی را برای شما ایجاد نخواهد کرد.
کتابخانه PdfReport به صورت سورس باز در CodePlex قرار گرفته ؛ اما جهت پرسیدن سؤالات، پیشنهادات، ارائه بهبود و غیره میتوانید (و بهتر است) از قسمت مدیریت پروژه مرتبط در سایت جاری نیز استفاده کنید.
نحوه تهیه اولین گزارش، با کتابخانه PdfReport
الف) یک پروژه Class library جدید را شروع کنید. از این جهت که گزارشات PdfReport در انواع و اقسام پروژههای VS.NET قابل استفاده است، میتوان از این پروژه Class library به عنوان کلاسهای پایه قابل استفاده در انواع و اقسام پروژههای مختلف، بدون نیاز به تغییری در کدهای آن استفاده کرد.
ب) آخرین نگارش فایلهای مرتبط با PdfReport را از اینجا دریافت کنید و سپس ارجاعاتی را به اسمبلیهای موجود در بسته آن به پروژه خود اضافه نمائید (ارجاعاتی به PdfReport، iTextSharp و EPPlus). فایل XML راهنمای کتابخانه نیز به همراه بسته آن میباشد که در حین استفاده از متدها و خواص PdfReport کمک بزرگی خواهد بود.
ج) کلاسهای زیر را به آن اضافه کنید:
using System.Web; using System.Windows.Forms; namespace PdfReportSamples { public static class AppPath { public static string ApplicationPath { get { if (isInWeb) return HttpRuntime.AppDomainAppPath; return Application.StartupPath; } } private static bool isInWeb { get { return HttpContext.Current != null; } } } }
همانطور که مشاهده میکنید ارجاعاتی را به System.Windows.Forms.dll و System.Web.dll نیاز دارد.
در ادامه کلاس User را جهت ساخت یک منبع داده درون حافظهای تعریف خواهیم کرد:
using System; namespace PdfReportSamples.IList { public class User { public int Id { set; get; } public string Name { set; get; } public string LastName { set; get; } public long Balance { set; get; } public DateTime RegisterDate { set; get; } } }
using System; using System.Collections.Generic; using PdfRpt.Core.Contracts; using PdfRpt.FluentInterface; namespace PdfReportSamples.IList { public class IListPdfReport { 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(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.ttf", Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf"); }) .PagesFooter(footer => { footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy")); }) .PagesHeader(header => { header.DefaultHeader(defaultHeader => { defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png"); defaultHeader.Message("گزارش جدید ما"); }); }) .MainTableTemplate(template => { template.BasicTemplate(BasicTemplate.ClassicTemplate); }) .MainTablePreferences(table => { table.ColumnsWidthsType(TableColumnWidthType.Relative); table.NumberOfDataRowsPerPage(5); }) .MainTableDataSource(dataSource => { var listOfRows = new List<User>(); for (int i = 0; i < 200; i++) { listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 }); } dataSource.StronglyTypedList<User>(listOfRows); }) .MainTableSummarySettings(summarySettings => { summarySettings.OverallSummarySettings("جمع کل"); summarySettings.PerviousPageSummarySettings("نقل از صفحه قبل"); 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<User>(x => x.Id); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(1); column.Width(2); column.HeaderCell("شماره"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Name); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(2); column.Width(3); column.HeaderCell("نام"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.LastName); column.CellsHorizontalAlignment(HorizontalAlignment.Center); column.IsVisible(true); column.Order(3); column.Width(3); column.HeaderCell("نام خانوادگی"); }); columns.AddColumn(column => { column.PropertyName<User>(x => x.Balance); 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 : string.Format("{0:n0}", obj)); }); column.AggregateFunction(aggregateFunction => { aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum); aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj)); }); }); }) .MainTableEvents(events => { events.DataSourceIsEmpty(message: "رکوردی یافت نشد."); }) .Export(export => { export.ToExcel(); export.ToCsv(); export.ToXml(); }) .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptIListSample.pdf")); } } }
var rpt = new IListPdfReport().CreatePdfReport(); // rpt.FileName
برای نمونه، جهت مشاهده نمایش این خروجی در یک برنامه ویندوزی، به مثالهای همراه سورس پروژه در مخزن کد آن مراجعه نمائید.
توضیحات بیشتر:
- در قسمت DocumentPreferences، جهت راست به چپ (PdfRunDirection)، اندازه صفحه (PdfPageSize)، جهت صفحه (PageOrientation) و امثال آن تنظیم میشوند.
- سپس نیاز است قلمهای مورد استفاده در گزارش مشخص شوند. در متد DefaultFonts باید دو قلم را معرفی کنید. قلم اول، قلم پیش فرض خواهد بود و قلم دوم برای رفع نواقص قلم اول مورد استفاده قرار میگیرد. برای مثال اگر قلم اول فاقد حروف انگلیسی است، به صورت خودکار به قلم دوم رجوع خواهد شد.
- در ادامه در متد PagesFooter، تاریخ درج شده در پایین تمام صفحات مشخص میشود. در مورد ساخت Footer سفارشی در قسمتهای بعدی بحث خواهد شد.
- در متد PagesHeader، متن و تصویر قرار گرفته در Header تمام صفحات گزارش قابل تنظیم است. این مورد نیز قابل سفارشی سازی است که در قسمتهای بعد به آن خواهیم پرداخت.
- توسط MainTableTemplate، قالب ظاهری ردیفهای گزارش مشخص میشود. یک سری قالب پیش فرض در کتابخانه PdfReport موجود است که توسط متد BasicTemplate آن قابل دسترسی است. در مورد نحوه تعریف قالبهای سفارشی به مرور در قسمتهای بعد، بحث خواهد شد.
- در قسمت MainTablePreferences تنظیمات جدول اصلی گزارش تعیین میشود. برای مثال چه تعداد ردیف در صفحه نمایش داده شود. اگر این مورد را تنظیم نکنید، به صورت خودکار محاسبه خواهد شد. نحوه تعیین عرض ستونهای گزارش به کمک متد ColumnsWidthsType مشخص میشود که در اینجا حالت نسبی درنظر گرفته شده است.
- منبع داده مورد استفاده توسط متد MainTableDataSource مشخص میشود که در اینجا یک لیست جنریک تعیین شده و سپس توسط متد StronglyTypedList در اختیار گزارش ساز جاری قرار میگیرد. تعدادی منبع داده پیش فرض در PdfReport وجود دارند که هر کدام را در قسمتهای بعدی بررسی خواهیم کرد. همچنین امکان تعریف منابع داده سفارشی نیز وجود دارد.
- با کمک متد MainTableSummarySettings، برچسبهای جمعهای پایین صفحات مشخص میشود.
- در قسمت MainTableColumns، ستونهایی را که علاقمندیم در گزارش ظاهر شوند، قید میکنیم. هر ستون باید با یک فیلد یا خاصیت منبع داده متناظر باشد. همچنین همانطور که مشاهده میکنید امکان تعیین Visibility، عرض و غیره آن نیز مهیا است (قابلیت ساخت گزارشاتی که به انتخاب کاربر، ستونهای آن ظاهر یا مخفی شوند). در اینجا توسط callbackهایی که در متد ColumnItemsTemplate قابل دسترسی هستند، میتوان اطلاعات را پیش از نمایش فرمت کرد. برای مثال سه رقم جدا کننده به اعداد اضافه کرد (برای نمونه در خاصیت موجودی فوق) و یا توسط متد AggregateFunction، میتوان متد تجمعی مناسبی را جهت ستون جاری مشخص کرد.
- توسط متد MainTableEvents به بسیاری از رخدادهای داخلی PdfReport دسترسی خواهیم یافت. برای مثال اگر در اینجا رکوردی موجود نباشد، رخداد DataSourceIsEmpty صادر خواهد شد.
- به کمک متد Export، خروجیهای دلخواه مورد نظر را میتوان مشخص کرد. تعدادی خروجی، مانند اکسل، XML و CSV در این کتابخانه موجود است. امکان سفارشی سازی آنها نیز پیش بینی شده است.
- و نهایتا توسط متد Generate مشخص خواهیم کرد که فایل گزارش کجا ذخیره شود.
لطفا برای طرح مشکلات و سؤالات خود در رابطه با کتابخانه PdfReport از این قسمت سایت استفاده کنید.