نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
نیازی نیست زیاد پیچیده فکر کنید. یک لیست ساده رو واکشی کنید (مثلا یک کامنت که سه زیر مجموعه مرتبط دارد با یک select ساده)، اتصال نهایی آن‌ها در سمت کلاینت خودکار است. به عبارتی شکل دهی تو در تو در اینجا در سمت کلاینت انجام می‌شود و نه سمت سرور. سمت سرور آن، یک select معمولی از رکوردهای مورد نظر بیشتر نیست.
var specifiedCommentId = 4; //عدد 4 در اینجا شماره رکورد کامنتی است که دارای یک سری زیر کامنت است
var list = ctx.BlogComments
                 .Include(x => x.Reply)
                 .Where(x => x.Id >= specifiedCommentId && (x.ReplyId == x.Reply.Id || x.Reply == null))
                 .ToList() // fills the childs list too
                 .Where(x => x.Reply == null) // for TreeViewHelper                        
                 .ToList();
نظرات مطالب
معرفی کتابخانه PdfReport
کلمات «هر گزارشی» و «هر شکلی» یعنی چی؟
اگر «هر شکلی» منظور ساخت گزارشات نامنظم است، شاید این روش استفاده از قالب‌های open office بهتر باشد.
بازخوردهای پروژه‌ها
خروجی تصویر (jpg یا png)
با سلام
آیا امکان گرفتن خروجی تصویر از گزارشات با فرمت‌های jpg و یا png امکان پذیر است؟چگونه؟
باتشکر
مطالب
ایجاد یک ActionFilter جهت تمیز کردن اطلاعات ورودی در ASP.NET Core
در  ASP.NET Core کار جلوگیری از حملات  XSS بر عهده برنامه نویس گذاشته شده‌است و مانند نسخه‌های قبلی، Request Validation یا اعتبارسنجی درخواست‌ها به صورت توکار در آن وجود ندارد. برای اطلاعات بیشتر به این مقاله مراجعه کنید.

هرچند ASP.NET Core داده‌ها را هنگام نمایش، encode می‌کند و عملا بسیاری از حملات خنثی می‌شوند، اما در صورتیکه بخواهیم داده‌های غیر مطمئن، در بانک اطلاعاتی نیز ذخیره نشوند، باید آنها را ارزیابی کنیم. یکی از روش‌های مقابله‌ی با این حملات، تمیز کردن اطلاعات ورودی کاربر است. در ادامه به ایجاد یک ActionFilter می‌پردازیم تا این کار را برای ورودی‌های یک Action Method انجام دهد:
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public class SanitizeInputAttribute : ActionFilterAttribute
    {
        var sanitizer = new Ganss.XSS.HtmlSanitizer(); 

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            if (filterContext.ActionArguments != null)
            {
                foreach (var parameter in filterContext.ActionArguments)
                {
                    var properties = parameter.Value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
                        .Where(x => x.CanRead && x.CanWrite && x.PropertyType == typeof(string) && x.GetGetMethod(true).IsPublic && x.GetSetMethod(true).IsPublic);
                    foreach (var propertyInfo in properties)
                    {
                        if (propertyInfo.GetValue(parameter.Value) != null)
                            propertyInfo.SetValue(parameter.Value,
                                sanitizer.Sanitize(propertyInfo.GetValue(parameter.Value).ToString()));  
                    }
                }
            }
        }
    }
در این فیلتر، از کتابخانه
HtmlSanitizer برای تمیز کردن اطلاعات استفاده کرده‌ایم. ابتدا تمام ورودی‌های اکشن متد را خوانده و سپس ورودی‌هایی را که از نوع  string هستند، پیدا کرده و مقدار فعلی آنها را با مقدار sanitize شده، جایگزین می‌کند. بنابراین اگر در رشته‌ی ورودی، عبارت یا تگ خطرناک یا غیر مجازی باشد، حذف می‌گردد.

برای استفاده از این فیلتر کافی است به صورت زیر عمل کنیم:
[SanitizeInput]
public IActionResult Add(GroupDto dto)
مطالب
ارسال خطاهای رخ‌داده‌ی در برنامه‌های سمت کلاینت Blazor WASM، به تلگرام
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ می‌دهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع می‌شود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ داده‌ی در برنامه‌ی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظه‌ی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیده‌ی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جمله‌ی معروف: «بک‌آپ سروری که روی همان سرور گرفته می‌شود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارش‌های رسیده‌ی به آن نیز بسیار ساده‌است و می‌توان این گزارش‌ها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشی‌های همراه، تبلت‌ها و غیره نیز بررسی کرد.




نحوه‌ی نمایش خطاها در برنامه‌های Blazor

در حین توسعه‌ی برنامه‌های Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر می‌شود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهده‌ی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ داده‌است اکتفا کرده و گزینه‌ی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد می‌دهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت می‌گیرد:
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
که شیوه نامه‌های پیش‌فرض آن در فایل wwwroot/css/app.css قرار دارند. در حالت عادی المان blazor-error-ui به همراه یک display: none است که از نمایش آن جلوگیری می‌کند. اما در زمان بروز خطایی، فریم‌ورک آن‌را به صورت display: block نمایش می‌دهد.


نحوه‌ی مدیریت استثناءها در برنامه‌های Blazor

توصیه شده‌است که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آن‌ها و یا stack-trace آن‌ها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آن‌ها اطلاعات ارزشمندی را از نحوه‌ی عملکرد برنامه بدست آورد.
برخلاف برنامه‌های ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آن‌ها می‌توان مدیریت سراسری خطاهای رخ‌داده را انجام داد، چنین ویژگی در برنامه‌های Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آن‌را تشکیل می‌دهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامه‌ی Blazor رخ‌دهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده می‌شود. در اینجا Console Logging Provider، تامین کننده‌ی پیش‌فرض سیستم ثبت وقایع برنامه‌های Blazor است. به همین جهت استثناءهای مدیریت نشده‌ی برنامه را می‌توان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه می‌شود.
بنابراین اگر در برنامه‌ی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آن‌را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، می‌توان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده می‌کند)، منجمله تمام خطاهای رخ‌داده (و مدیریت نشده) مطلع شد و برای مثال آن‌ها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامه‌ی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.


نوشتن یک ILoggerProvider سفارشی جهت ارسال رخ‌دادها برنامه‌ی سمت کلاینت، به یک Web API

برای ارسال تمام وقایع برنامه‌ی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class ClientLoggerProvider : ILoggerProvider
    {
        private readonly HttpClient _httpClient;
        private readonly WebApiLoggerOptions _options;
        private readonly NavigationManager _navigationManager;

        public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
        {
            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>();
            _options = options.Value;
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new WebApiLogger(_httpClient, _options, _navigationManager);
        }

        public void Dispose()
        {
        }
    }
}
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، روش آن به صورت زیر است:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public static class ClientLoggerProviderExtensions
    {
        public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>();
            return builder;
        }
    }
}
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی می‌کند و نحوه‌ی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویس‌های برنامه در سازنده‌ی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگی‌ها، سازنده‌های ClientLoggerProvider را تامین می‌کند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده می‌کنید:
public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
با استفاده از IServiceProvider می‌توان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیش‌فرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازنده‌ی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم می‌کند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر می‌کند، به صورت پیش‌فرض دارای طول عمر Singleton هستند و می‌توان آن‌ها را بدون مشکل، به سازنده‌ی لاگر سفارشی، تزریق کرد.
مهم‌ترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت می‌دهد:
public ILogger CreateLogger(string categoryName)
{
   return new WebApiLogger(_httpClient, _options, _navigationManager);
}
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLogger : ILogger
    {
        private readonly WebApiLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly NavigationManager _navigationManager;

        public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public IDisposable BeginScope<TState>(TState state) => default;

        public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter is null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            try
            {
                ClientLog log = new()
                {
                    LogLevel = logLevel,
                    EventId = eventId,
                    Message = formatter(state, exception),
                    Exception = exception?.Message,
                    StackTrace = exception?.StackTrace,
                    Url = _navigationManager.Uri
                };
                _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log);
            }
            catch
            {
                // don't throw exceptions from the logger
            }
        }
    }
}
نحوه‌ی عملکرد این ILogger سفارشی بسیار ساده‌است:
- متد IsEnabled آن مشخص می‌کند که چه سطحی از رخ‌دادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت می‌کند:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLoggerOptions
    {
        public string LoggerEndpointUrl { set; get; }

        public LogLevel LogLevel { get; set; } = LogLevel.Information;
    }
}
در این تنظیمات مشخص می‌کنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "WebApiLogger": {
    "LogLevel": "Warning",
    "LoggerEndpointUrl": "/api/logs"
  }
}
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگی‌ها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازنده‌ی تامین کننده‌ی لاگر را پیدا کند:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            // …
        }
    }
}
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آن‌را ارسال می‌کند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کرده‌ایم که بین برنامه‌ی کلاینت و سرور، مشترک است:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Shared
{
    public class ClientLog
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public string Message { get; set; }

        public string Exception { get; set; }

        public string StackTrace { get; set; }

        public string Url { get; set; }
    }
}
این اطلاعاتی است که کلاینت به ازای رخ‌دادی خاص، جمع آوری کرده و به سمت سرور ارسال می‌کند.

در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام می‌دهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            builder.Services.AddLogging(configure =>
            {
                configure.AddWebApiLogger();
            });

            await builder.Build().RunAsync();
        }
    }
}
تا اینجا اگر هر نوع استثنای مدیریت نشده‌ای در برنامه‌ی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شده‌ی در فایل Client\wwwroot\appsettings.json است:
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحله‌ی بعدی، تکمیل کنترلر یاد شده‌است.


ایجاد سرویسی برای ارسال لاگ‌های برنامه به سمت تلگرام

پیش از اینکه کار تکمیل کنترلر api/logs را در برنامه‌ی Web API انجام دهیم، ابتدا در همان برنامه‌ی Web API، سرویسی را برای ارسال لاگ‌های رسیده به سمت تلگرام، تهیه می‌کنیم. علت اینکه این قسمت را به برنامه‌ی سمت سرور محول کرده‌ایم، شامل موارد زیر است:
- درست است که می‌توان کتابخانه‌های مرتبط با تلگرام را به برنامه‌ی سی‌شارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیم‌تر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آن‌را در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامه‌ی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامه‌ی سمت کلاینت به Web API، مزیت لاگ سمت سرور آن‌را مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر می‌کند و صرفا محدود به تلگرام نیست.

برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه می‌کنیم:
using System;
using System.Text;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;

namespace BlazorWasmTelegramLogger.Server.Services
{
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }

    public interface ITelegramBotService
    {
        Task SendLogAsync(ClientLog log);
    }

    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }

        public async Task SendLogAsync(ClientLog log)
        {
            var text = formatMessage(log);
            if (string.IsNullOrWhiteSpace(text))
            {
                return;
            }

            await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown);
        }

        private static string formatMessage(ClientLog log)
        {
            if (string.IsNullOrWhiteSpace(log.Message))
            {
                return string.Empty;
            }

            var sb = new StringBuilder();
            sb.Append(toEmoji(log.LogLevel))
                .Append(" *")
                .AppendFormat("{0:hh:mm:ss}", DateTime.Now)
                .Append("* ")
                .AppendLine(log.Message);

            if (!string.IsNullOrWhiteSpace(log.Exception))
            {
                sb.AppendLine()
                    .Append('`')
                    .AppendLine(log.Exception)
                    .AppendLine(log.StackTrace)
                    .AppendLine("`")
                    .AppendLine();
            }

            sb.Append("*Url:* ").AppendLine(log.Url);
            return sb.ToString();
        }

        private static string toEmoji(LogLevel level) =>
            level switch
            {
                LogLevel.Trace => "⬜️",
                LogLevel.Debug => "🟦",
                LogLevel.Information => "⬛️️️",
                LogLevel.Warning => "🟧",
                LogLevel.Error => "🟥",
                LogLevel.Critical => "❌",
                LogLevel.None => "🔳",
                _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
            };
    }
}
توضیحات:
- برای کار با API تلگرام، از کتابخانه‌ی معروف Telegram.Bot استفاده کرده‌ایم که به صورت زیر، وابستگی آن به برنامه‌ی Web API اضافه می‌شود:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Telegram.Bot" Version="15.7.1" />
  </ItemGroup>
</Project>
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }
- برای دریافت AccessToken، در برنامه‌ی تلگرام خود، بات مخصوصی را به نام https://t.me/botfather یافته و سپس آن‌را استارت کنید:


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

- مرحله‌ی بعد تنظیم ChatId است. نحوه‌ی کار برنامه به این صورت است که پیام‌ها را به این بات سفارشی خود ارسال کرده و این بات، آن‌ها را به کانال اختصاصی ما هدایت می‌کند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان می‌کند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کرده‌ایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظه‌ای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن می‌توانید مشاهده کنید:


در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامه‌ی شما است.

در آخر این اطلاعات را در فایل Server\appsettings.json قرار می‌دهیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "TelegramLoggingBot": {
    "AccessToken": "1826…",
    "ChatId": "-1001…" 
  }
}
که نحوه‌ی ثبت و معرفی آن‌ها به سیستم تزریق وابستگی‌های برنامه‌ی Web API، به صورت زیر است:
namespace BlazorWasmTelegramLogger.Server
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TelegramLoggingBotOptions>(options =>
                            Configuration.GetSection("TelegramLoggingBot").Bind(options));
            services.AddSingleton<ITelegramBotService, TelegramBotService>();

            // ...
        }

        // ...
    }
}
سرویس ITelegramBotService را با طول عمر Singleton معرفی کرده‌ایم. چون new TelegramBotClient ای که در سازنده‌ی آن صورت می‌گیرد:
    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی می‌گردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال می‌شود.


ایجاد کنترلر Logs، جهت دریافت لاگ‌های رسیده‌ی از سمت کلاینت

مرحله‌ی آخر کار بسیار ساده‌است. سرویس تکمیل شده‌ی ITelegramBotService را به سازنده‌ی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آن‌را فراخوانی می‌کنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Server.Services;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class LogsController : ControllerBase
    {
        private readonly ILogger<LogsController> _logger;
        private readonly ITelegramBotService _telegramBotService;

        public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _telegramBotService = telegramBotService;
        }

        [HttpPost]
        public async Task<IActionResult> PostLog(ClientLog log)
        {
            // TODO: Save the client's `log` in the database

            _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message);

            await _telegramBotService.SendLogAsync(log);

            return Ok();
        }
    }
}


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

برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار داده‌ایم:
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        throw new InvalidOperationException("This is an exception message from the client!");
    }
}
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمه‌ی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmTelegramLogger.zip
مطالب دوره‌ها
پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC
مدتی است در اکثر سایت‌ها و طراحی‌های جدید، به جای استفاده از روش متداول نمایش انتخاب صفحه 1، 2 ... 100، برای صفحه بندی اطلاعات، از روش اسکرول نامحدود یا infinite scroll استفاده می‌کنند. نمونه‌ای از آن‌را هم در سایت جاری با دکمه «بیشتر» در ذیل اکثر صفحات و مطالب سایت مشاهده می‌کنید.


در ادامه قصد داریم نحوه پیاده سازی آن‌را در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.


مدل برنامه

namespace jQueryMvcSample02.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
در این برنامه و مثال، قصد داریم لیستی از مطالب را توسط اسکرول نامحدود، نمایش دهیم. هر آیتم نمایش داده شده، ساختاری همانند کلاس BlogPost دارد.


منبع داده فرضی برنامه

using System.Collections.Generic;
using System.Linq;
using jQueryMvcSample02.Models;

namespace jQueryMvcSample02.DataSource
{
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
برای اینکه برنامه نهایی را به سادگی بتوانید اجرا کنید، به عمد از بانک اطلاعاتی خاصی استفاده نشده و صرفا یک منبع داده فرضی تشکیل شده در حافظه، در اینجا مورد استفاده قرار گرفته است. بدیهی است قسمت cachedItems را به سادگی می‌توانید با یک ORM جایگزین کنید.
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts می‌باشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازه‌ای از اطلاعات که قرار است نمایش داده شوند، دریافت می‌گردد. خوشبختانه این متدها معادل‌های مناسبی را در اکثر بانک‌های اطلاعاتی داشته و استفاده از آن‌ها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار می‌کند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.


کنترلر برنامه

using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample02.DataSource;
using jQueryMvcSample02.Security;

namespace jQueryMvcSample02.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            //آغاز کار با صفحه صفر است
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(list); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult PagedIndex(int? page)
        {
            var pageNumber = page ?? 0;
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber);
            if (list == null || !list.Any())
                return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها

            return PartialView("_ItemsList", list);
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
کنترلر برنامه را در اینجا ملاحظه می‌کنید. برای کار با اسکرول نامحدود، به ازای هر صفحه، نیاز به دو متد است:
الف) یک متد که بر اساس HttpGet کار می‌کند. این متد در اولین بار نمایش صفحه فراخوانی می‌گردد و اطلاعات صفحه آغازین را نمایش می‌دهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستی‌های AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیک‌های کاربر بر روی دکمه «بیشتر» استفاده می‌گردد. بنابراین تنها کاری که افزونه جی‌کوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده می‌شود.


دو View برنامه

همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر می‌کند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <h5>
                @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new  { id = item.Id },
                                 htmlAttributes: null)
            </h5>
            @item.Body
        </li>
    }
</ul>
ب) یک View کامل که در بار اول نمایش صفحه، مورد استفاده قرار می‌گیرد:
@model IList<jQueryMvcSample02.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home");
}
<h2>
    اسکرول نامحدود</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
<div id="MoreInfoDiv">
</div>
<div align="center" style="margin-bottom: 9px;">
    <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span>
</div>
<div id="ProgressDiv" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#moreInfoButton").InfiniteScroll({
                moreInfoDiv: '#MoreInfoDiv',
                progressDiv: '#ProgressDiv',
                loadInfoUrl: '@loadInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد
                },
                noMoreInfoHandler: function () {
                    alert('اطلاعات بیشتری یافت نشد');
                }
            });
        });
    </script>
}
چند نکته در اینجا حائز اهمیت است:
1) مسیر دقیق اکشن متد PagedIndex توسط متد  Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده می‌کنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایل‌های آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک می‌کند، توسط افزونه جی‌کوئری ما نمایان شده و در پایان کار مجددا مخفی می‌گردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام می‌دهد.


و کدهای افزونه اسکرول نامحدود

// <![CDATA[
(function ($) {
    $.fn.InfiniteScroll = function (options) {
        var defaults = {
            moreInfoDiv: '#MoreInfoDiv',
            progressDiv: '#Progress',
            loadInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            noMoreInfoHandler: null
        };
        var options = $.extend(defaults, options);

        var showProgress = function () {
            $(options.progressDiv).css("display", "block");
        }

        var hideProgress = function () {
            $(options.progressDiv).css("display", "none");
        }       

        return this.each(function () {
            var moreInfoButton = $(this);
            var page = 1;
            $(moreInfoButton).click(function (event) {
                showProgress();
                $.ajax({
                    type: "POST",
                    url: options.loadInfoUrl,
                    data: JSON.stringify({ page: page }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else {
                            if (data == "no-more-info") {
                                if (options.noMoreInfoHandler)
                                    options.noMoreInfoHandler(this);
                            }
                            else {
                                var $boxes = $(data);
                                $(options.moreInfoDiv).append($boxes);
                            }
                            page++;
                        }
                        hideProgress();
                        if (options.completeHandler)
                            options.completeHandler(this);
                    }
                });
            });
        });
    };
})(jQuery);
// ]]>
ساختار افزونه اسکرول نامحدود به این شرح است:
هربار که کاربر بر روی دکمه بیشتر کلیک می‌کند، progress div ظاهر می‌گردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال می‌گردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه می‌کند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی می‌کند.

دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip
 
نظرات مطالب
نحوه پیاده سازی عملیات Undo و Redo با استفاده از الگوی طراحی Command
چه جوری میشه از  الگوی طراحی Command  در Mvc استفاده کرد؟
بنده چند نمونه پیدا کرده ام مثل
ولی برام یکم گنگ هست اگه میشه راهنمایی کنید 
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت دوم

در مقاله‌ی قبل توانستیم یک سری از مدل‌های مربوط به وبلاگ را آماده کنیم. در ادامه به تکمیل آن و همچین آغاز تهیه‌ی مدل‌های مربوط به اخبار و پیغام خصوصی می‌پردازیم.
همکاران این قسمت:
سلمان معروفی

مدل گزارش دهی

    /// <summary>    
    /// Repersents a Report template for every cms section
    /// </summary>
    public class Report
    {
        #region Ctor
        /// <summary>
        /// Create one instance for <see cref="Report"/>
        /// </summary>
        public Report()
        {
            ReportedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier for Report
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets reason of report
        /// </summary>
        public virtual string Reason { get; set; }
        /// <summary>
        /// gets or sets section that is reported
        /// </summary>
        public virtual ReportSection Section { get; set; }
        /// <summary>
        /// gets or sets sectionid that is reported
        /// </summary>
        public virtual long SectionId { get; set; }
        /// <summary>
        /// gets or sets type of report
        /// </summary>
        public virtual ReportType Type{ get; set; }
        /// <summary>
        /// gets or sets report's datetime
        /// </summary>
        public virtual DateTime ReportedOn { get; set; }
        /// <summary>
        /// indicate this report is read by admin
        /// </summary>
        public virtual bool IsRead { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets id of user that is reporter
        /// </summary>
        public virtual long ReporterId { get; set; }
        /// <summary>
        /// gets or sets id of user that is reporter
        /// </summary>
        public virtual User Reporter { get; set; }
        #endregion
    }

/// <summary>
    /// Represents Report Section
    /// </summary>
   public  enum  ReportSection
    {
        News,
        Poll,
        Announcement,
        ForumTopic,
        BlogComment,
        BlogPost,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        User,
      ...
    }

/// <summary>
    /// Represents Type of Report
    /// </summary>
    public enum  ReportType
    {
        Spam,
        Abuse,
        Advertising,
       ...
    }

قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخش‌های مختلف را بدهیم. این دسترسی‌ها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان می‌تواند برای مدیریت سیستم مفید باشد.
برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection  از نوع داده‌ی شمارشی می‌باشد که در بالا تعریف آن نیز آماده است و مشخص کننده‌ی بخش‌هایی می‌باشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType می‌باشد، مشخص کننده‌ی نوع گزارشی است که داده شده است. 
علاوه بر نوع گزارش، می‌توان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شده‌است. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقاله‌ی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و  BaseComment خواهید شد که برای نشان دادن تعداد گزارش‌هایی است که برای آن مطلب یا نظر داده شده است، استفاده می‌شود.

کلاس پایه فایل‌های ضمیمه

 /// <summary>
    /// Represents a base class for every attachment
    /// </summary>
    public abstract class BaseAttachment
    {
        #region Ctor

        public BaseAttachment()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            AttachedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier for attachment
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets name for attachment
        /// </summary>
        public virtual string FileName { get; set; }
        /// <summary>
        /// sets or gets type of attachment
        /// </summary>
        public virtual string ContentType { get; set; }
        /// <summary>
        /// sets or gets size of attachment
        /// </summary>
        public virtual long Size { get; set; }
        /// <summary>
        /// sets or gets Extention of attachment
        /// </summary>
        public virtual string Extension { get; set; }
        /// <summary>
        /// sets or gets bytes of data
        /// </summary>
        //public byte[] Data { get; set; }
        /// <summary>
        /// sets or gets Creation Date
        /// </summary>
        public virtual DateTime AttachedOn { get; set; }
        /// <summary>
        /// gets or sets counts of download this file
        /// </summary>
        public virtual long DownloadsCount { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets section that this file attached there
        /// </summary>
        public virtual AttachmentSection Section { get; set; }
        /// <summary>
        /// gets or sets information of user agent 
        /// </summary>
        public virtual string Agent { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual long OwnerId { get; set; }
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual User Owner { get; set; }
        #endregion
    }



    public enum  AttachmentSection
    {
        News,
        Announcement,
        ForumTopic,
        Conversation,
        BlogComment,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        BlogPost,
        Group,
        ...
    }

کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم از ارث بری TPH برای مدیریت فایل‌های ضمیمه استفاده کنیم. در سیستم بسته‌ی ما، تنها کاربران احراز هویت شده می‌توانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کننده‌ی فایل می‌باشد، به صورت Nullable در نظر نگرفته‌ایم.
یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایل‌های ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخش‌های (وبلاگ - اخبار - نظرسنجی‌ها - آگهی‌ها - انجمن)  که نیاز به Privacy خاصی نیست و احراز هویت کفایت می‌کند، مدل زیر را در نظر گرفته ایم:

مدل فایل‌های ضمیمه عمومی

 /// <summary>
    /// Repersent the attachment for file
    /// </summary>
    public class Attachment : BaseAttachment
    {
       
    }
  مدل بالا صرفا برای بخش‌های مذکور کفایت خواهد کرد. در ادامه مقالات، برای بخش‌هایی مانند پیغام خصوصی، گروه‌هایی که کاربران ایجاد می‌کنند، برای انتشار تجربیات خود و هر بخشی که اضافه شود و نیاز به Privacy داشته باشد، نیاز خواهند بود تا مدل Attachment آنها با خود بخش هم در ارتباط باشد و تمام خصوصیت آنها که اکثرا کلید خارجی خواهند بود به صورت Nullable تعریف شوند.
مدل اخبار
 /// <summary>
    /// Represents one news item 
    /// </summary>
    public class NewsItem : BaseContent
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="NewsItem"/>
        /// </summary>
        public NewsItem()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// indicating that this news show on sidebar
        /// </summary>
        public virtual bool ShowOnSideBar { get; set; }
        /// <summary>
        /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }

        #endregion

        #region NavigationProperties

        /// <summary>
        /// gets or sets  newsitem's Reviews
        /// </summary>
        public ICollection<NewsComment> Comments { get; set; }

        #endregion
    }

                  کلاس بالا نشان دهنده‌ی اشتراک‌های ما خواهند بود. این مدل ما هم از کلاس پایه‌ی BaseContent بحث شده در مقاله‌ی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .

مدل نظرات اخبار 

 public class NewsComment : BaseComment
    {
        #region Ctor
        public NewsComment()
        {
            Rating = new Rating();
            CreatedOn = DateTime.Now;

        }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual NewsComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual ICollection<NewsComment> Children { get; set; }
        /// <summary>
        /// gets or sets NewsItem that this comment sent to it
        /// </summary>
        public virtual NewsItem NewsItem { get; set; }
        /// <summary>
        /// gets or sets NewsItem'Id that this comment sent to it
        /// </summary>
        public virtual long NewsItemId { get; set; }
        #endregion
    }

                           مدل بالا نشان دهنده‌ی نظرات داده شده‌ی برای اخبار می‌باشند که از کلاس BaseComment بحث شده در مقاله‌ی قبل ارث بری کرده و ساختار درختی آن نیز مشخص است و همچنین برای اعمال ارتباط یک به چند نیز خصوصیتی تحت عنوان NewsItem  با کلید NewsItemId در این کلاس در نظر گرفته شده است.

مدل‌های پیغام خصوصی
/// <summary>
    /// Indicate one conversation
    /// </summary>
    public class Conversation
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Conversation"/>
        /// </summary>
        public Conversation()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            SentOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// represents this conversaion is seen
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets subject of this conversation
        /// </summary>
        public virtual string Subject { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime SentOn { get; set; }
        /// <summary>
        /// indicate this record deleted by sender
        /// </summary>
        public virtual bool DeletedBySender { get; set; }
        /// <summary>
        /// indicate this record deleted by receiver
        /// </summary>
        public virtual bool DeletedByReceiver { get; set; }
        /// <summary>
        /// gets or sets Messagescount that Unread  by sender of this conversation
        /// </summary>
        public virtual int UnReadSenderMessagesCount { get; set; }
        /// <summary>
        /// gets or sets Messagescount that Unread  by receiver of this conversation
        /// </summary>
        public virtual int UnReadReceiverMessagesCount { get; set; }
        /// <summary>
        /// gets or sets Messagescount of this conversation for increase performance
        /// </summary>
        public virtual int MessagesCount { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets if of  user that start this conversation
        /// </summary>
        public virtual long SenderId { get; set; }
        /// <summary>
        /// gets or sets user that start this conversation
        /// </summary>
        public virtual User Sender { get; set; }
        /// <summary>
        /// gets or sets id of  user that is recipient
        /// </summary>
        public virtual long ReceiverId { get; set; }
        /// <summary>
        /// gets or sets   user that is recipient
        /// </summary>
        public virtual User Receiver { get; set; }
        /// <summary>
        /// get or set Messages of this conversation
        /// </summary>
        public virtual ICollection<ConversationReply> Messages { get; set; }
        /// <summary>
        /// get or set Attachments that attached in this conversation
        /// </summary>
        public virtual ICollection<ConversationAttachment> Attachments { get; set; }
        #endregion

مدل بالا نشان دهنده‌ی گفتگوی بین دو کاربر می‌باشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر به‌عنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفته‌ایم.
خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شده‌اند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شده‌اند و در واقع تعداد پیغام‌های خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره می‌شود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReply‌ها را در مدل بالا تعریف کرده‌ایم.
در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.   
مدل  ConversationReply به شکل زیر می‌باشد:

  /// <summary>
    /// Represents One Reply to Conversation
    /// </summary>
    public class ConversationReply
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ConversationReply"/>
        /// </summary>
        public ConversationReply()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            SentOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// represents this conversaionReply is seen
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets body of this conversationReply
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime SentOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets  Parent's Id Of this ConversationReply
        /// </summary>
        public virtual Guid? ParentId { get; set; }
        /// <summary>
        /// gets or sets Parent Of this ConversationReply
        /// </summary>
        public virtual ConversationReply Parent { get; set; }
        /// <summary>
        /// get or set Children Of this ConversationReply
        /// </summary>
        public virtual ICollection<ConversationReply> Children { get; set; }
        /// <summary>
        /// gets or sets if of  user that start this conversationReply
        /// </summary>
        public virtual long SenderId { get; set; }
        /// <summary>
        /// gets or sets user that start this conversationReply
        /// </summary>
        public virtual User Sender { get; set; }
        /// <summary>
        /// gets or sets Conversation that this message sent in it 
        /// </summary>
        public virtual Conversation Conversation{ get; set; }
        /// <summary>
        /// gets or sets Id of Conversation that this message sent in it 
        /// </summary>
        public virtual Guid ConversationId { get; set; }
        #endregion
    }

مدل بالا نشان دهنده‌ی پیغام‌های داده شده در یک گفتگو با موضوعی خاص می‌باشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغام‌ها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفته‌ایم.  
با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگو‌ها به شکل زیر باشد:

/// <summary>
    /// Represents the attachment That attached in Conversation
    /// </summary>
    public class ConversationAttachment : BaseAttachment
    {
        #region NavigationProperties

        public virtual Conversation Conversation { get; set; }
        public virtual Guid? ConversationId { get; set; }
        #endregion
    }

همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPH را برای مدیریت فایل‌های ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شده‌است.

نتیجه این قسمت

مطالب
RadioButtonList در ASP.NET MVC

برای تهیه یک 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")