مسیرراه‌ها
WPF
          مطالب دوره‌ها
          خلاصه‌ای از اعمال متداول با AutoMapper و Entity Framework
          فرض کنید کلاس‌های مدل برنامه از سه کلاس مشتری، سفارشات مشتری‌ها و اقلام هر سفارش تشکیل شده‌است:
          public class Customer
          {
              public int Id { set; get; }
              public string FirstName { get; set; }
              public string LastName { get; set; }
              public string Bio { get; set; }
           
              public virtual ICollection<Order> Orders { get; set; }
           
              [Computed]
              [NotMapped]
              public string FullName
              {
                  get { return FirstName + ' ' + LastName; }
              }
          }
          
          public class Order
          {
              public int Id { set; get; }
              public string OrderNo { get; set; }
              public DateTime PurchaseDate { get; set; }
              public bool ShipToHomeAddress { get; set; }
           
              public virtual ICollection<OrderItem> OrderItems { get; set; }
           
              [ForeignKey("CustomerId")]
              public virtual Customer Customer { get; set; }
              public int CustomerId { get; set; }
           
              [Computed]
              [NotMapped]
              public decimal Total
              {
                  get { return OrderItems.Sum(x => x.TotalPrice); }
              }
          }
          
          public class OrderItem
          {
              public int Id { get; set; }
              public decimal Price { get; set; }
              public string Name { get; set; }
              public int Quantity { get; set; }
           
              [ForeignKey("OrderId")]
              public virtual Order Order { get; set; }
              public int OrderId { get; set; }
           
              [Computed]
              [NotMapped]
              public decimal TotalPrice
              {
                  get { return Price * Quantity; }
              }
          }
          در اینجا برای پیاده سازی خواص محاسباتی، از نکته‌ی مطرح شده‌ی در مطلب «نگاشت خواص محاسبه شده به کمک AutoMapper و DelegateDecompiler» استفاده شده‌است.
          در ادامه می‌خواهیم اطلاعات حاصل از این کلاس‌ها را با شرایط خاصی به ViewModelهای مشخصی جهت نمایش در برنامه نگاشت کنیم.


          نمایش اطلاعات مشتری‌ها

          می‌خواهیم اطلاعات مشتری‌ها را مطابق فرمت کلاس ذیل بازگشت دهیم:
          public class CustomerViewModel
          {
              public string Bio { get; set; }
              public string CustomerName { get; set; }
          }
          با این شرایط که
          - اگر Bio نال بود، بجای آن N/A نمایش داده شود.
          - CustomerName از خاصیت محاسباتی FullName کلاس مشتری تامین گردد.

          برای حل این مساله، نیاز است نگاشت زیر را تهیه کنیم:
          this.CreateMap<Customer, CustomerViewModel>()
             .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(entity => entity.FullName))
             .ForMember(dest => dest.Bio, opt => opt.MapFrom(entity => entity.Bio ?? "N/A"));
          AutoMapper برای جایگزین کردن خواص با مقدار نال، با یک مقدار مشخص، از متدی به نام NullSubstitute استفاده می‌کند. اما در این حالت خاص که قصد داریم از Project To استفاده کنیم، این روش پاسخ نمی‌دهد و محدودیت‌هایی دارد. به همین جهت از روش map from و بررسی مقدار خاصیت، استفاده شده‌است.
          همچنین در اینجا مطابق نگاشت فوق، خاصیت CustomerName از خاصیت FullName کلاس مشتری دریافت می‌شود.

          کوئری نهایی استفاده کننده‌ی از این اطلاعات به شکل زیر خواهد بود:
          using (var context = new MyContext())
          {
              var viewCustomers = context.Customers
                  .Project()
                  .To<CustomerViewModel>()
                  .Decompile()
                  .ToList();
              // don't use
              // var viewCustomers = Mapper.Map<IEnumerable<Customer>, IEnumerable<CustomerViewModel>>(dbCustomers);
              foreach (var customer in viewCustomers)
              {
                  Console.WriteLine("{0} - {1}", customer.CustomerName, customer.Bio);
              }
          }
          در اینجا از متدهای Project To و همچنین Decompile استفاده شده‌است (جهت پردازش خاصیت محاسباتی).


          نمایش اطلاعات سفارش‌های مشتری‌ها

          در ادامه قصد داریم اطلاعات سفارش‌ها را با فرمت ViewModel ذیل نمایش دهیم:
          public class OrderViewModel
          {
              public string CustomerName { get; set; }
              public decimal Total { get; set; }
              public string OrderNumber { get; set; }
              public IEnumerable<OrderItemsViewModel> OrderItems { get; set; }
          }
          
          public class OrderItemsViewModel
          {
              public string Name { get; set; }
              public int Quantity { get; set; }
              public decimal Price { get; set; }
          }
          با این شرایط که
          - CustomerName از خاصیت محاسباتی FullName کلاس مشتری تامین گردد.
          - خاصیت OrderNumber آن از خاصیت OrderNo تهیه گردد.

          به همین جهت کار را با تهیه‌ی نگاشت ذیل ادامه می‌دهیم:
          this.CreateMap<Order, OrderViewModel>()
            .ForMember(dest => dest.OrderNumber, opt => opt.MapFrom(src => src.OrderNo))
            .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.FullName));
          بر این اساس کوئری مورد استفاده نیز به نحو ذیل تشکیل می‌شود:
          using (var context = new MyContext())
          {
              var viewOrders = context.Orders
                  .Project()
                  .To<OrderViewModel>()
                  .Decompile()
                  .ToList();
              // don't use
              // var viewOrders = Mapper.Map<IEnumerable<Order>, IEnumerable<OrderViewModel>>(dbOrders);
              foreach (var order in viewOrders)
              {
                  Console.WriteLine("{0} - {1} - {2}", order.OrderNumber, order.CustomerName, order.Total);
              }
          }
          در اینجا چون از خاصیت OrderItems کلاس ViewModel صرفنظر نشده‌است، اطلاعات آن نیز به همراه viewOrders موجود است. یعنی می‌توان چنین کوئری را نیز جهت نمایش اطلاعات تو در توی اقلام هر سفارش نیز نوشت:
          using (var context = new MyContext())
          {
              var viewOrders = context.Orders
                  .Project()
                  .To<OrderViewModel>()
                  .Decompile()
                  .ToList();
              // don't use
              // var viewOrders = Mapper.Map<IEnumerable<Order>, IEnumerable<OrderViewModel>>(dbOrders);
              foreach (var order in viewOrders)
              {
                  Console.WriteLine("{0} - {1} - {2}", order.OrderNumber, order.CustomerName, order.Total);
                  foreach (var item in order.OrderItems)
                  {
                      Console.WriteLine("({0}) {1} - {2}", item.Quantity, item.Name, item.Price);
                  }
              }
          }
          اگر می‌خواهید OrderItems به صورت خودکار واکشی نشود، نیاز است در نگاشت تهیه شده، توسط متد Ignore از آن صرفنظر کنید:
          this.CreateMap<Order, OrderViewModel>()
            .ForMember(dest => dest.OrderNumber, opt => opt.MapFrom(src => src.OrderNo))
            .ForMember(dest => dest.OrderItems, opt => opt.Ignore())
            .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.FullName));


          نمایش اطلاعات یک سفارش، با فرمتی خاص

          تا اینجا نگاشت‌های انجام شده بر روی لیستی از اشیاء صورت گرفتند. در ادامه می‌خواهیم اولین سفارش ثبت شده را با فرمت ذیل نمایش دهیم:
          public class OrderDateViewModel
          {
              public int PurchaseHour { get; set; }
              public int PurchaseMinute { get; set; }
              public string CustomerName { get; set; }
          }
          به همین منظور ابتدا نگاشت ذیل را تهیه می‌کنیم:
          this.CreateMap<Order, OrderDateViewModel>()
            .ForMember(dest => dest.PurchaseHour, opt => opt.MapFrom(src => src.PurchaseDate.Hour))
            .ForMember(dest => dest.PurchaseMinute, opt => opt.MapFrom(src => src.PurchaseDate.Minute))
            .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.FullName));
          در اینجا ساعت و دقیقه‌ی خرید، از خاصیت PurchaseDate استخراج شده‌اند. همچنین CustomerName نیز از خاصیت FullName کلاس مشتری دریافت گردیده‌است.
          پس از این تنظیمات، کوئری نهایی به شکل ذیل خواهد بود:
          using (var context = new MyContext())
          {
              var viewOrder = context.Orders
                  .Project()
                  .To<OrderDateViewModel>()
                  .Decompile()
                  .FirstOrDefault();
              // don't use
              // var viewOrder = Mapper.Map<Order, OrderDateViewModel>(dbOrder);
           
              if (viewOrder != null)
              {
                  Console.WriteLine("{0}, {1}:{2}", viewOrder.CustomerName, viewOrder.PurchaseHour, viewOrder.PurchaseMinute);
              }
          }


          فرمت کردن سفارشی اطلاعات در حین نگاشت‌ها

          در مورد فرمت کننده‌های سفارشی و تبدیلگرها پیشتر بحث کرده‌ایم. اما اغلب آن‌ها را در حالت خاص LINQ to Entities نمی‌توان بکار برد، زیرا قابلیت تبدیل به SQL را ندارند. برای مثال فرض کنید می‌خواهیم خاصیت ShipToHomeAddress کلاس Order را به خاصیت ShipHome کلاس ذیل نگاشت کنیم:
          public class OrderShipViewModel
          {
              public string ShipHome { get; set; }
              public string CustomerName { get; set; }
          }
          با این شرط که اگر مقدار آن True بود، Yes را نمایش دهد. با توجه به ساختار مدنظر، نگاشت ذیل را می‌توان تهیه کرد که در آن فرمت کردن سفارشی، به متد MapFrom واگذار شده‌است:
          this.CreateMap<Order, OrderShipViewModel>()
             .ForMember(dest => dest.ShipHome, opt => opt.MapFrom(src=>src.ShipToHomeAddress? "Yes": "No"))
             .ForMember(dest => dest.CustomerName, opt => opt.MapFrom(src => src.Customer.FullName));
          با این کوئری جهت استفاده‌ی از این تنظیمات:
          using (var context = new MyContext())
          {
              var viewOrders = context.Orders
                  .Project()
                  .To<OrderShipViewModel>()
                  .Decompile()
                  .ToList();
              // don't use
              // var viewOrders = Mapper.Map<IEnumerable<Order>, IEnumerable<OrderShipViewModel>>(dbOrders);
              foreach (var order in viewOrders)
              {
                  Console.WriteLine("{0} - {1}", order.CustomerName, order.ShipHome);
              }
          }

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

          همونطور که مطلع هستین پَرباد برای بانک اطلاعاتی از EntityFramework استفاده می‌کنه. بنابراین این پروژه Migrations‌های مخصوص به خودش رو داره که با هر آپدیت میتونن اعمال بشن. در نتیجه شما نمی‌تونید با پروژه خودتون Merge کنید. 

          بانک اطلاعاتی پَرباد فقط جنبه مصرف داخلی داره. اما حتی اگر اینگونه هم نبود، شما نباید طراحی سیستم خودتون رو بر اساس یک کتابخانه (پَرباد و یا هر کتابخانه دیگری) انجام بدید. طراحی سیستم شما باید کاملا مستقل از هر ابزاری باشه.
          به این علت که:
          1. این ابزار توسط اشخاص دیگری توسعه داده شده نه شما و این یعنی هر لحظه امکان تغییر سراسری اون ابزار توسط توسعه دهندگانش هست. در نتیجه هر لحظه‌ای که اون ابزار تغییری پیدا بکنه، شما هم باید طراحی سیستم خودتون رو تغییر بدید.
          2. ذخیره اطلاعات یک پرداخت باید توسط شما در بانک اطلاعاتی شما انجام بشه، اطلاعاتی که پَرباد در بانک اطلاعاتی خودش ذخیره و بازبابی می‌کنه، صرفا جنبه مصرف داخلی برای خودش رو داره.

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

          اگر بخوام کامل و مرحله‌ای براتون توضیح بدم، عملیات استاندارد خرید یا سفارش به صورت زیر هست:

          1. شما از قبل طراحی بانک اطلاعاتی خودتون رو بدون در نظر گرفتن هیج گونه ابزار خارجی (پَرباد) انجام داده‌اید. (پَرباد یک ابزار پرداخت هست و برای اون اهمیتی نداره پرداخت در سیستم مصرف کننده به چه شکلی طراحی شده. وظیفه او فقط انجام عملیات پرداخت آنلاین هست)
          2. مبلغ قابل پرداخت رو مشخص می‌کنید و درخواست پرداخت رو توسط پَرباد انجام میدید.
          3. نتیجه درخواست پرداخت که شامل کد رهگیری و غیره هست رو در بانک اطلاعاتی خودتون ثبت می‌کنید. (برای فاکتور مورد نظر)
          4. کاربر به درگاه بانکی هدایت میشه،  هزینه رو پرداخت می‌کنه و به وب سایت شما برمیگرده.
          5. عملیات تایید پرداخت رو توسط پَرباد انجام میدید.
          6. پس از تایید، کلیه اطلاعات لازم مانند کد رهگیری، کد تراکنش بانکی، مبلغ و غیره رو از پَرباد دریافت می‌کنید و در بانک اطلاعاتی خودتون ذخیره می‌کنید (با توجه به کد رهگیری که در مرحله ۳ ذخیره کرده بودید، اطلاعات فاکتور مورد نظرتون رو آپدیت می‌کنید)

          مطالب
          طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید

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


          یک مثال واقعی

          در زمان ثبت یک Task، کاربر می‌تواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شماره‌ای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین می‌کند که شماره یکتایی را ایجاد کند.

          ایجاد یک قرارداد برای موجودیت‌های دارای شماره منحصر به فرد
          public interface INumberedEntity
          {
              string Number { get; set; }
          }
          با استفاده از این واسط می‌توان از تکرار یکسری از تنظیمات مانند تنظیم طول فیلد Number و همچنین ایجاد ایندکس منحصر به فرد برروی آن، به شکل زیر جلوگیری کرد.
          foreach (var entityType in builder.Model.GetEntityTypes()
              .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType)))
          {
              builder.Entity(entityType.ClrType)
                  .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50);
          
              if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType))
              {
                  builder.Entity(entityType.ClrType)
                      .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId))
                      .HasName(
                          $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}")
                      .IsUnique();
              }
              else
              {
                  builder.Entity(entityType.ClrType)
                      .HasIndex(nameof(INumberedEntity.Number))
                      .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}")
                      .IsUnique();
              }
          }

          ایجاد یک Entity برای نگهداری شماره قابل استفاده بعدی مرتبط با موجودیت‌ها
          public class NumberedEntity : Entity, IMultiTenantEntity
          {
              public string EntityName { get; set; }
              public long NextNumber { get; set; }
              
              public long TenantId { get; set; }
          }

          با تنظیمات زیر:
          public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity>
          {
              public void Configure(EntityTypeBuilder<NumberedEntity> builder)
              {
                  builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false);
                  builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique();
                  builder.ToTable(nameof(NumberedEntity));
              }
          }

          شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت می‌توان آخرین شماره ثبت شده‌ی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:

          • ایجاد Gap مابین شماره‌های تولید شده، که مدنظر ما نمی‌باشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شماره‌ای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)


          پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number

          internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity>
          {
              private readonly IUnitOfWork _uow;
              private readonly IOptions<NumberingConfiguration> _configuration;
          
              public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration)
              {
                  _uow = uow ?? throw new ArgumentNullException(nameof(uow));
                  _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
              }
          
              protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata)
              {
                  if (!entity.Number.IsNullOrEmpty()) return;
          
                  bool retry;
                  string nextNumber;
                  
                  do
                  {
                      nextNumber = GenerateNumber(entity);
                      var exists = CheckDuplicateNumber(entity, nextNumber);
                      retry = exists;
                      
                  } while (retry);
                  
                  entity.Number = nextNumber;
              }
          
              private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber)
              {
                 //...
              }
          
              private string GenerateNumber(INumberedEntity entity)
              {
                 //...
              }
          }

          ابتدا بررسی می‌شود اگر پراپرتی Number مقداردهی شده‌است، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شماره‌ای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت می‌گیرد.


          بررسی متد GenerateNumber

          private string GenerateNumber(INumberedEntity entity)
          {
              var option = _configuration.Value.NumberedEntityOptions[entity.GetType()];
          
              var entityName = $"{entity.GetType().FullName}";
          
              var lockKey = $"Tenant_{_uow.TenantId}_" + entityName;
          
              _uow.ObtainApplicationLevelDatabaseLock(lockKey);
          
              var nextNumber = option.Start.ToString();
          
              var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName);
              if (numberedEntity == null)
              {
                  _uow.ExecuteSqlCommand(
                      "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName,
                      option.Start + option.IncrementBy, _uow.TenantId);
              }
              else
              {
                  nextNumber = numberedEntity.NextNumber.ToString();
                  _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ",
                      numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id);
              }
          
              if (!string.IsNullOrEmpty(option.Prefix))
                  nextNumber = option.Prefix + nextNumber;
              
              return nextNumber;
          }

          ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد می‌کنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنش‌های همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید می‌باشد، با توجه به اینکه INumberedEntity می‌باشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.

          پیاده سازی متد مذکور به شکل زیر می‌باشد:

          public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource)
          {
              uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, 
                          @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000);
          }

          با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیت‌های INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونه‌ها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی می‌شود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود. 

          گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری می‌باشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام می‌گیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.

          ساختار NumberingConfiguration

          public class NumberingConfiguration
          {
              public bool Enabled { get; set; }
          
              public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } =
                  new Dictionary<Type, NumberedEntityOption>();
          }
          public class NumberedEntityOption
          {
              public string Prefix { get; set; }
              public int Start { get; set; } = 1;
              public int IncrementBy { get; set; } = 1;
          }

          با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیت‌ها را خواهیم داشت.

          گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیت‌ها

          public static void AddNumbering(this IServiceCollection services,
              IDictionary<Type, NumberedEntityOption> options)
          {
              services.Configure<NumberingConfiguration>(configuration =>
              {
                  configuration.Enabled = true;
                  configuration.NumberedEntityOptions.AddRange(options);
              });
              
              services.AddTransient<IPreActionHook, NumberingPreInsertHook>();
          }

          و استفاده از این متد الحاقی در Startup پروژه

          services.AddNumbering(new Dictionary<Type, NumberedEntityOption>
          {
              [typeof(Task)] = new NumberedEntityOption
              {
                  Prefix = "T_",
                  Start = 1000,
                  IncrementBy = 5
              }
          });

          و موجودیت Task

          public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
          {
              public const int MaxTitleLength = 256;
              public const int MaxDescriptionLength = 1024; 
          
              public string Title { get; set; }
              public string NormalizedTitle { get; set; }
              public string Description { get; set; }
              public TaskState State { get; set; } = TaskState.Todo; 
              public byte[] RowVersion { get; set; }
              public string Number { get; set; }
          }

          با خروجی‌های زیر

          پ.ن ۱: در برخی از Domain‌ها نیاز به ریست کردن این شماره‌ها براساس یکسری فیلد موجود در موجودیت مورد نظر نیز مطرح می‌باشد. به عنوان مثال در یک سیستم انبارداری شاید براساس FiscalYear و در یک سیستم فروش با توجه به نحوه فروش (SaleType)، لازم باشد این ریست برای شماره‌های موجودیت «سفارش»، انجام پذیرد. در کل با کمی تغییرات می‌توان از این روش مطرح شده در چنین حالاتی نیز به عنوان یک ابزار شماره گذاری خودکار کمک گرفت.
          پ.ن ۲: استفاده از امکانات  Sequence در Sql Server هم شاید اولین راه حلی باشد که به ذهن می‌رسد؛ ولی از آنجایی که از تراکنش‌ها پشتیبانی ندارد، مسئله Gap بین شماره‌ها پابرجاست و همچنین آزادی عملی را به این شکل که در مطلب مطرح شد، نداریم.
          مطالب
          تقویم شمسی در ویندوز 10
          امروز بعد از چندین سال، شاید بعد از 5 سال، ویندوز 7 نسخه Home Premium را به Windows 10 Home ارتقاء دادم. واقعا این روزها دیگر ویندوز 7 در انجام کارها یاری نمی‌کرد و بصورت مداوم خطای صفحه‌ی آبی را نمایش می‌داد. ولی در حین گشت و گذار وب بودم که بصورت اتفاقی به این لینک برخوردم. بعد از مطالعه‌ی لینک و مراجعه به لینک اصلی متوجه شدم مایکروسافت این امکان را به کاربران نسخه اصلی ویندوزهای قبلی داده است که بتوانند بصورت رایگان ویندوزشان را به ویندوزی معادل، بشرح جدول زیر ارتقا دهند و خوشبختانه بدون کمترین زحمت و مشقتی توانستم یک نسخه‌ی پاک و به روز را با IP ایرانی از سایت مایکروسافت بشرح زیر دریافت کنم و تجربه‌ی جدیدی داشته باشم:


          ابتدا باید فایل Media Creation Tool نسخه‌ی 64بیتی را دانلود کنید. بوسیله‌ی این نرم افزار می‌توانید نسخه‌ی ISO یا نسخه‌ی برخط و آنلاین را دریافت کنید. بعد از دریافت فایل ISO، بوسیله‌ی یه نرم افزار مانند Rufus فایل ISO را می‌تونید به یک فلش Bootable تبدیل کنید؛ یا اینکه بر روی DVD رایت کنید. در صورتیکه قصد ارتقاء نسخه‌ی اصلی ویندوز فعلی خودتان را داشته باشید، نصاب Media Creation Tool از شما شماره‌ی سریال نرم افزار را درخواست نمی‌کند. در غیر اینصورت اگر قصد داشته باشید یک نصب از ابتدا (Clean Installation) را داشته باشید، باید شماره سریال معتبر محصول قبلی را جهت فعالسازی وارد نمایید. روال و فرآیند نصب که خیلی سهل و آسان است و نیازی به توضیح ندارد. ولی یک امکان عالی که به نسخه‌ی جدید ویندوز اضافه شده‌است، پشتیبانی از تقویم فارسی هست. همانطور که مایکروسافت وعده‌ی آن را داده بود:


          این مورد یکی از مهم‌ترین تغییرات لااقل برای ما ایرانی‌ها است. بعبارت دیگر در هر جا که تاریخ میلادی وجود داشته باشد، به تاریخ شمسی تبدیل خواهد شد. به عنوان مثال امکان مرتب سازی بر اساس تاریخ شمسی بی نقص امکان پذیر است:


          و یا بعنوان مثال دیگر تاریخ خصیصه‌ها به فرمت تاریخ شمسی نمایش داده می‌شود.



          و مانند سایر تقویم‌ها امکان سفارش نمودن آن وجود دارد.


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



          جایگزین کردن یک Subgrid با یک jqGrid کامل

          خلاصه‌ی عملیات جایگزینی یک Subgrid را توسط یک jqGrid کامل، در ذیل مشاهده می‌کنید:
                      $('#list').jqGrid({
                          caption: "آزمایش دوازدهم",
                          //..........مانند قبل
                          subGrid: true,
                          subGridRowExpanded: grid1RowExpanded
                      });
          
                  function grid1RowExpanded(subGridId, rowId) {
                      var subgridTableId = subGridId + "_t";
                      var pagerId = "p_" + subgridTableId;
                      var container = 'g_' + subGridId;
                      $("#" + subGridId).html('<div dir="rtl" id="' + container + '" style="width:100%; height: 100%">' +
                          '<table id="' + subgridTableId + '" class="scroll"></table><div id="'
                          + pagerId + '" class="scroll"></div>');
          
                      var url = '@Url.Action("GetOrderDetails", "Home", routeValues: new { id = "js-id" })'
                                .replace("js-id", encodeURIComponent(rowId)); // تزریق اطلاعات سمت کاربر به خروجی سمت سرور
          
                      $("#" + subgridTableId).jqGrid({
                          caption: "ریز اقلام سفارش " + rowId,
                          autoencode: true, //security - anti-XSS
                          url: url,
                          //..........مانند قبل
                      });
                  }
          همانند نمایش subgridهای معمولی، ابتدا subGrid: true باید اضافه شود تا ستونی با ردیف‌های + دار، ظاهر شود. اینبار توسط روال رویدادگردان subGridRowExpanded، کنترل نمایش subgrid را در دست گرفته و آن‌را با یک jqGrid جایگزین می‌کنیم.
          امضای متد grid1RowExpanded، شامل id یک div است که گرید جدید در آن قرار خواهد گرفت، به همراه Id ردیفی که اطلاعات زیرگرید آن نیاز است از سرور واکشی شود.
          بر مبنای subGridId، مانند قبل، یک جدول و یک div را برای نمایش jgGrid و pager آن به صفحه به صورت پویا اضافه می‌کنیم.
          سپس تعاریف jqGrid آن مانند قبل است و  نکته‌ی خاصی ندارد. بدیهی است گرید جدید نیز می‌تواند در صورت نیاز یک subgrid دیگر داشته باشد.
          در اینجا تنها نکته‌ی مهم آن نحوه‌ی ارسال اطلاعات rowId به سرور است. اکشن متدی که قرار است اطلاعات زیر گرید را تامین کند، یک چنین امضایی دارد:
             public ActionResult GetOrderDetails(int id, JqGridRequest request)
          بنابراین نیاز است که به نحوی rowId را به آن ارسال کرد. مشکل اینجا است که Url.Action یک کد سمت سرور است و rowId یک متغیر سمت کاربر. نمی‌توان این متغیر را در کدهای Razor مستقیما قرار داد. اما می‌توان یک محل جایگزینی را در کدهای سمت سرور پیش بینی کرد. مثلا js-id. زمانیکه این رشته در صفحه رندر می‌شود، به صورت معمول و به کمک متد replace جاوا اسکریپت، js-id آن‌را با rowId جایگزین می‌کنیم. به این ترتیب امکان تزریق اطلاعات سمت کاربر به خروجی سمت سرور Razor میسر می‌شود.


          کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
          jqGrid12.zip
          بازخوردهای پروژه‌ها
          تنظیم کردن فونت برای گزارش
          با عرض سلام.  من برای ایجاد گزارش خودم کدهای زیر را نوشتم.
          public IPdfReportData CreatePdfReport(int type,int RequestId)
                  {
                      return new PdfReport().DocumentPreferences(doc =>
                      {
                          doc.RunDirection(PdfRunDirection.RightToLeft);
                          doc.Orientation(PageOrientation.Portrait);
                          doc.PageSize(PdfPageSize.A4);
                         
                      })
                      .DefaultFonts(fonts =>
                      {
                          fonts.Path(AppPath.ApplicationPath + "\\Fonts\\BNAZANIN.TTF",
                                            AppPath.ApplicationPath + "\\Fonts\\TIMES.TTF");
                          fonts.Size(20);
                      })
                      .PagesFooter(footer =>
                      {
                          footer.DefaultFooter("");
                          footer.PdfFont.Size = 8;
          
                      })
                      .PagesHeader(header =>
                      {
                          if (type == 1)
                          {
                              header.CustomHeader(_customHeader);
                          }
                          else
                          {
                              header.DefaultHeader(h => h.Message("mohsen"));
                          }
          
                      })
                       .MainTableTemplate(template =>
                       {
                           template.BasicTemplate(BasicTemplate.SilverTemplate);
                       })
                      .MainTablePreferences(table =>
                      {
                          table.ColumnsWidthsType(TableColumnWidthType.Relative);
          
          
                      })
                      .MainTableDataSource(dataSource =>
                      {
                         
                          
                          var ctx = new ClearanceEntities();
          
          
          
          
                          var list = (from c in ctx.CLEARANCE_ITEMS
                                      where c.CLEARANCE_REQUEST.REQUEST_ID == RequestId
                                      select new
                                      {
                                          c.TARIFF_NO,
                                          c.GOODS_DESCRIPTION,
                                          vahed = c.QUANTITY,
                                          c.PACKING_TYPES.PACKING_NAME,
                                          c.GROSS_WEIGHT,
                                          arzesh = (c.GOODS_PRICE * c.GOODS_CURRENCY_RATE) + (c.FREIGHT_PRICE * c.FREIGHT_CURRENCY_RATE),
                                          hoghogh = " ",
                                          sood = " "
          
                                      }).ToList();
                         
          
          
          
                          dataSource.AnonymousTypeList(list );
                      })
          
                      .MainTableColumns(columns =>
                      {
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("TARIFF_NO");
          
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(0);
                              column.Width(3);
                              column.HeaderCell("تعرفه");
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("GOODS_DESCRIPTION");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
          
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(5);
                              column.HeaderCell("نام کالا");
                              column.ColumnItemsTemplate(template =>
                                  {
                                      new CellBasicProperties
                                          {
                                            
                                              //PdfFontStyle = DocumentFontStyle.Bold | DocumentFontStyle.Underline,
                                              //FontColor = new BaseColor(System.Drawing.Color.Brown),
                                              //BackgroundColor = new BaseColor(System.Drawing.Color.Yellow)
                                          };
                                      return;
                                  });
          
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("vahed");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(5);
                              column.HeaderCell("واحد");
          
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("GROSS_WEIGHT");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(5);
                              column.HeaderCell("وزن ناخالص");
          
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("arzesh");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(5);
                              column.HeaderCell("ارزش دلاری");
          
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("hoghogh");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(3);
                              column.HeaderCell("حقوق گمرکی");
          
                          });
                          columns.AddColumn(column =>
                          {
                              column.PropertyName("sood");
                              column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                              column.IsVisible(true);
                              column.Order(1);
                              column.Width(3);
                              column.HeaderCell("سود بازرگانی");
          
                          });
          
          
                      })
                      .MainTableEvents(events =>
                      {
                          events.DataSourceIsEmpty(message: "There is no data available to display.");
          
          }
          
                           events.DocumentClosing(args =>
                      {
                          if (HttpContext.Current == null || HttpContext.Current.Response == null) return;
          
                          // close the document without closing the underlying stream
                          args.PdfWriter.CloseStream = false;
                          args.PdfDoc.Close();
                          args.PdfStreamOutput.Position = 0;
          
                          // write pdf bytes to output stream
                          var pdf = ((MemoryStream)args.PdfStreamOutput).ToArray();
                          string str = Guid.NewGuid().ToString();
                          HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                          HttpContext.Current.Response.ContentType = MediaTypeNames.Application.Pdf;
                          HttpContext.Current.Response.AddHeader("Content-Length", pdf.Length.ToString());
                          HttpContext.Current.Response.AddHeader("content-disposition", "attachment;filename=" + str + ".pdf");
                          HttpContext.Current.Response.Buffer = true;
                          HttpContext.Current.Response.Clear();
                          HttpContext.Current.Response.OutputStream.Write(pdf, 0, pdf.Length);
                          HttpContext.Current.Response.OutputStream.Flush();
                          HttpContext.Current.Response.OutputStream.Close();
                          HttpContext.Current.Response.End();
                      });
                  })
          
                     .Generate(data => data.AsPdfStream(new MemoryStream()));

          همانطور که در قسمت DefaultFonts   اندازه فونت را 20 تعریف کردم ولی هیچ تاثیری در فونت گزارش من داده نمی‌شود. ممنون میشم راهنمایی کنید.

          مطالب
          توصیف فیلدها توسط Tag Helper و Data annotation

          همه ما با DisplayAttribute در DataAnnotaion آشنا هستیم. چیزی شبیه زیر برای یک موجودیت:

          public class Student{
              [Display(Name="نام خانوادگی")]
              public string FamilyName { get; set;}
          }

          با استفاده از tag helper ای به نام asp-for می‌توان متادیتای Name را به کاربر، در سمت رابط کاربری نشان داد؛ برای مثال:

          <label asp-for="FamilyName"></label>

          و یا موقع اعتبارسنجی می‌توان به جای نشان دادن نام FamilyName از نام مفهوم‌تری مانند نام خانوادگی استفاده نمود.

          چه خوب بود اگر می‌شد علاوه بر نام، توصیفی از فیلد نیز برای آن در این قسمت وجود داشته باشد؛ به عبارت دیگر اگر کد زیر را داشتیم:

          [Display(
               Name = "نام خانوادگی",
               Description = "بهتر است فقط در اینجا نام خانوادگی شخص وارد شود")]
          public string FamilyName{ get; set; }

          بتوان از tag helper ای مانند زیر استفاده نمود:

          <span asp-description-for="FamilyName"></span>

          که در نهایت چنین خروجی html ای داشته باشیم:

          <span>بهتر است فقط در اینجا نام خانوادگی شخص وارد شود</span>

          برای این منظور می‌توان از کلاس زیر بهره برد:

          using Microsoft.AspNetCore.Mvc.Rendering;
          using Microsoft.AspNetCore.Mvc.ViewFeatures;
          using Microsoft.AspNetCore.Razor.TagHelpers;
          
          [HtmlTargetElement("div", Attributes = ForAttributeName)]
          [HtmlTargetElement("p", Attributes = ForAttributeName)]
          [HtmlTargetElement("span", Attributes = ForAttributeName)]
          public sealed class DescriptionForTagHelper : TagHelper
          {
              private const string ForAttributeName = "asp-description-for";
          
              [HtmlAttributeName(ForAttributeName)] 
              public ModelExpression For { get; set; } = default!;
          
              public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
              {
                  if (context == null)
                  {
                      throw new ArgumentNullException(nameof(context));
                  }
          
                  if (output == null)
                  {
                      throw new ArgumentNullException(nameof(output));
                  }
          
                  var description = For.Metadata.Description;
                  if (description != null)
                  {
                      // Do not update the content if another tag helper
                      // targeting this element has already done so.
                      if (!output.IsContentModified)
                      {
                          var childContent = await output.GetChildContentAsync();
                          if (childContent.IsEmptyOrWhiteSpace)
                          {
                              output.Content.SetHtmlContent(description);
                          }
                          else
                          {
                              output.Content.SetHtmlContent(childContent);
                          }
                      }
                  }
              }
          }

          کلاس DescriptionForTagHelper از کلاس پایه TagHelper ارث بری نموده است و متد ProcessAsync آن به نحوی که  asp-description-for را بپذیرد override شده است.

          حوزه اعمال این tag helper به span، p و div محدود شده است؛ اما می‌توان با گذاشتن یک ستاره (*) آن را به کل المان‌های html اعمال کرد.

          مطالب
          مدیریت استثناءها در Blazor Server - قسمت اول
          همانطور که می‌دانید Blazor Server یک فریم ورک stateful است. هنگامیکه کاربران در حال تعامل با برنامه هستند، یک ارتباط پیوسته را با سرور حفظ می‌کنند که به آن، به اصطلاح مدار می‌گویند. این مدارها، کامپوننت‌های فعال را به انضمام حالت‌های آنها که شامل موارد زیر است نگهداری می‌کند:
          1- جدیدترین خروجی رندر شده‌ی کامپوننت.
          2- مجموعه Event Handling‌های جاری که می‌توانند توسط کاربر صدا زده شوند.
          اگر کاربری یک برنامه را در چندین تب مرورگر باز کند، در واقع چندین مدار مستقل را ایجاد کرده‌است. بنابراین اگر در یکی از تب‌های مرورگر استثنایی رخ دهد، مابقی تب‌های مرورگر متاثر نخواهند شد.
          Blazor با اکثریت استثناءهای کنترل نشده در  مداری که در آن رخ می‌دهد، خیلی بد رفتار می‌کند. چرا؟
          پاسخ: زیرا  کاربر فقط می‌تواند با بارگذاری مجدد آن تب مرورگر (برای ایجاد یک مدار جدید) به تعامل با برنامه ادامه دهد.
          حال برای رفع این مشکل چکار باید کرد؟ آیا راه حل سراسری برای مدیریت استثناها وجود دارد؟
          پاسخ: بله. 

          Error boundary

          یک کامپوننت از پیش تعریف شده‌ی Blazor است که رویکرد آسان آن برای مدیریت استثناءها به شکل زیر است:
          • هنگامیکه خطایی رخ نداده است، محتوای فرزند خود را رندر می‌کند. 

          • هنگامیکه یک استثناء کنترل نشده رخ می‌دهد، صفحه‌ی خطای پیش فرضی را رندر می‌کند. 

          برای استفاده از این کامپوننت، فقط کافی است محتوای مورد نظر خود را داخل آن بگذارید. برای مثال می‌توان، برای سراسری تعریف کردن Error boundary، به شکل زیر در فایل  Shared/MainLayout.razor   آن را تعریف نمود:
          <div>
              <div>
                  <ErrorBoundary>
                      @Body
                  </ErrorBoundary>
              </div>
          </div>
          در این حالت هر استثنای کنترل نشده‌ای که در کل برنامه رخ دهد، توسط Error boundaries کنترل شده و خطایی در صفحه نشان داده می‌شود. به صورت پیش فرض کامپوننت Error boundary یک div خالی را با یک کلاس css که در site.css وجود دارد، به نام blazor-error-boundary   به عنوان صفحه خطا نشان می‌دهد که می‌توان آن را سفارشی سازی نمود. همچنین می‌توان به شکل زیر نیز برای سفارشی سازی بیشتر صفحه‌ی خطا عمل کرد:
          <ErrorBoundary>
              <ChildContent>
                  @Body
              </ChildContent>
              <ErrorContent>
                  <p class="errorUI">متاسفانه خطایی رخ داده است!</p>
              </ErrorContent>
          </ErrorBoundary>
          به دلیل اینکه ما در این مثال Error boundary را در MainLayout تعریف کردیم، صفحه‌ی نمایش خطا صرفنظر از اینکه کاربر به کدام صفحه رفته‌است، نمایش داده می‌شود. پیشنهاد مایکروسافت این است که حوزه استفاده را محدودتر کنیم.
          خوب تا اینجای کار توانستیم استثنای کنترل نشده را کنترل کنیم و پیغام خطایی را نشان دهیم؛ اما همچنان صفحه در حالت خطا مانده و بازهم نیاز است که صفحه بارگذاری مجدد شود تا بتوان به صفحات دیگر برنامه رفت. آیا راه حلی وجود دارد؟
          پاسخ: بله خوشبختانه. کافی است با استفاده از متد Recover کامپوننت Error boundary به شکل زیر صفحه را به حالت قبل از خطا برد:
          ...
          
          <ErrorBoundary @ref="errorBoundary">
              @Body
          </ErrorBoundary>
          
          ...
          
          @code {
              private ErrorBoundary? errorBoundary;
          
              protected override void OnParametersSet()
              {
                  errorBoundary?.Recover();
              }
          }
          در قسمت بعدی به این موضوع می‌پردازیم که چگونه می‌توان یک کامپوننت خطای سفارشی سراسری ایجاد کرد تا علاوه بر کنترل استثناءها بتواند خطاها را نیز لاگ کند.
          مطالب
          Performance در AngularJS قدم سوم
          خیلی خوشحالم که تا این مرحله، این مقاله‌ها را دنبال می‌کنید. در مقالات قبل مسائل ساده و مهمی در بحث Performance مطرح شد. در این مقاله می‌خواهم قدم سوم در بهبود Performance را توضیح دهم که رعایت کردن این مسائل می‌تواند کمک زیادی در بهبود عملکرد برنامه‌های مبتنی بر AngularJS داشته باشد.

          scope؟
          همه‌ی برنامه نویسان و توسعه دهندگان، یکی از اولین مفاهیمی را که در AngularJS یاد می‌گیرند، scope هست. اما scope چیست؟ به صورت خیلی ساده می‌توان گفت scope مشخص کننده‌ی حوزه متغیر‌ها و توابعی هست که قرار است در View تاثیر داشته باشند. کد زیر را مشاهده کنید:
          <div>نام و نام خانوادگی: {{name}}</div>
          <div>معدل: {{avg()}}<div>
          و کد سمت controller
          scope.nums=[19,20,17,16,15,18,19];
          scope.name='بهنام محمدی';
          scope.avg= function(){
            return scope.nums.reduce(function(previousValue, currentValue) {
                return previousValue + currentValue;
             })/scope.nums.length;
          }
          و اما خروجی نهایی
          نام و نام خانوادگی: بهنام محمدی
          معدل:17.71
          خوب چون ما در قسمت controller به صورت scope.name و scope.avg عمل کرده‌ایم، می‌توانیم در View به صورت name و avg از این متغیر‌ها استفاده کنیم. در نتیجه اگر ما در controller، به جای scope.name بنویسیم name و یا به جای scope.avg بنویسیم avg به مشکل بر می‌خوریم؛ چون قسمت View ما متغیر‌های داخل scope را در View دخیل می‌کند و متغیر‌های داخل scope توسط Watcher‌ها رصد می‌شود.

          خوب سؤال، همه چیز که عالی هست پس مشکل در کجاست؟
          مشکل در متغیر scope.nums هست! به کد زیر توجه کنید:
          var nums=[19,20,17,16,15,18,19];
          scope.name='بهنام محمدی';
          scope.avg= function(){
            return nums.reduce(function(previousValue, currentValue) {
                return previousValue + currentValue;
             })/nums.length;
          }
          فکر کنم متوجه تفاوت این کد با کد بالایی شده‌اید. اما کدام کد درست است؟ یا بهتر بگویم کدام کد بر روی Performance تاثیر مناسبی دارد؟ کد پایینی Performance بالایی دارد. دلیل این موضوع این است وقتی ما از nums در View هیچ استفاده‌ای نمی‌کنیم، بهتر است به صورت var nums تعریف شود. در کد بالایی که این متغیر به صورت scope.nums تعریف شده بود، با اینکه در View استفاده نشده بود، ولی توسط Watcher AngularJS در هر لحظه رصد می‌شود و این کار باعث کندی و کاهش عملکرد AngularJS خواهد شد. بنابراین در کل متغیرهایی را که در View استفاده نمی‌کنید، به صورت var test استفاده نمایید تا Watcher AngularJS این متغیر‌ها را رصد نکند.
          امیدوارم از این مقاله لذت برده باشید. منتظر مقاله بعدی من باشید.