مطالب
نمایش ساختارهای درختی در Blazor
یکی از نکات جالب رندر کامپوننت‌ها در Blazor، امکان فراخوانی بازگشتی آن‌ها است؛ یعنی یک کامپوننت می‌تواند خودش را نیز فراخوانی کند. از همین قابلیت می‌توان جهت نمایش ساختارهای درختی، مانند مدل‌های خود ارجاع دهنده‌ی EF استفاده کرد.


مدل برنامه، جهت تامین داده‌های خود ارجاع دهنده و درختی

فرض کنید قصد داریم لیستی از کامنت‌های تو در تو را مدل سازی کنیم که در آن هر کامنت، می‌تواند چندین کامنت تا بی‌نهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels;

public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
برای نمونه بر اساس این مدل، منبع داده‌ی فرضی زیر را تهیه می‌کنیم:
using BlazorTreeView.ViewModels;

namespace BlazorTreeView.Pages;

public partial class TreeView
{
    private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } =
        new Dictionary<string, object>(StringComparer.Ordinal)
        {
            { "style", "list-style: none;" },
        };

    private IList<Comment> Comments { get; } =
        new List<Comment>
        {
            new()
            {
                Text = "پاسخ یک",
            },
            new()
            {
                Text = "پاسخ دو",
                Comments =
                    new List<Comment>
                    {
                        new()
                        {
                            Text = "پاسخ اول به پاسخ دو",
                            Comments =
                                new List<Comment>
                                {
                                    new()
                                    {
                                        Text = "پاسخی به پاسخ اول پاسخ دو",
                                    },
                                },
                        },
                        new()
                        {
                            Text = "پاسخ دوم به پاسخ دو",
                        },
                    },
            },
            new()
            {
                Text = "پاسخ سوم",
            },
        };
}
این قطعه کد partial class که مربوط به فایل TreeView.razor.cs برنامه‌است، در حقیقت کدهای پشت صحنه‌ی کامپوننت مثال TreeView.razor است که در ادامه آن‌را توسعه خواهیم داد. در نهایت قرار است بتوانیم آن‌را به صورت زیر رندر کنیم:



طراحی کامپوننت DntTreeView

برای اینکه بتوانیم به یک کامپوننت با قابلیت استفاده‌ی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary>
///   A custom DntTreeView
/// </summary>
public partial class DntTreeView<TRecord>
{
چون باید بتوان یک لیست جنریک <IEnumerable<TRecord را به آن، جهت رندر ارسال کرد و قرار نیست این کامپوننت، تنها به شیء سفارشی Comment مثال جاری ما وابسته باشد. بنابراین اولین خاصیت آن، شیء جنریک Items است که لیست کامنت‌ها/عناصر را دریافت می‌کند:
    /// <summary>
    ///     The treeview's self-referencing items
    /// </summary>
    [Parameter]
    public IEnumerable<TRecord>? Items { set; get; }
- هنگام رندر هر آیتم کامنت باید بتوان یک قالب سفارشی را از کاربر دریافت کرد. نمی‌خواهیم صرفا برای مثال Text شیء Comment فوق را به صورت متنی و ساده نمایش دهیم. می‌خواهیم در حین رندر، کل شیء TRecord جاری را به مصرف کننده ارسال و یک قالب سفارشی را از آن دریافت کنیم. یعنی باید یک RenderFragment جنریک را به صورت زیر نیز داشته باشیم تا مصرف کننده بتواند TRecord در حال رندر را دریافت و قالب Htmlای خودش را بازگشت دهد:
    /// <summary>
    ///     The treeview item's template
    /// </summary>
    [Parameter]
    public RenderFragment<TRecord>? ItemTemplate { set; get; }
- همچنین همیشه باید به فکر عدم وجود اطلاعاتی برای نمایش نیز بود. به همین جهت بهتر است قالب دیگری را نیز از مصرف کننده برای اینکار درخواست کنیم و نحوه‌ی رندر سفارشی این قسمت را نیز به مصرف کننده واگذار کنیم:
    /// <summary>
    ///     The content displayed if the list is empty
    /// </summary>
    [Parameter]
    public RenderFragment? EmptyContentTemplate { set; get; }
- زمانیکه با شیء از پیش تعریف شده‌ی Comment این مثال کار می‌کنیم، کاملا مشخص است که خاصیت Comments آن تو در تو است:
public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
اما زمانیکه با یک کامپوننت جنریک کار می‌کنیم، نیاز است از مصرف کننده، نام این خاصیت تو در تو را به نحو واضحی دریافت کنیم؛ به صورت زیر:
    /// <summary>
    ///     The property which returns the children items
    /// </summary>
    [Parameter]
    public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
دلیل استفاده از Expression Funcها را در مطلب «static reflection» می‌توانید مطالعه کنید. زمانیکه قرار است از کامپوننت DntTreeView استفاده کنیم، ابتدا نوع جنریک آن‌را مشخص می‌کنیم، سپس لیست اشیاء ارسالی به آن‌را و در ادامه با استفاده از ChildrenSelector به صورت زیر، مشخص می‌کنیم که خاصیت Comments است که به همراه Children می‌باشد و تو در تو است:
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
و مرسوم است جهت بالابردن کارآیی Expression Funcها، آن‌ها را کامپایل و کش کنیم که نمونه‌ای از روش آن‌را به صورت زیر مشاهده می‌کنید:
public partial class DntTreeView<TRecord>
{
    private Expression? _lastCompiledExpression;
    internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; }

    // ...

    protected override void OnParametersSet()
    {
        if (_lastCompiledExpression != ChildrenSelector)
        {
            CompiledChildrenSelector = ChildrenSelector?.Compile();
            _lastCompiledExpression = ChildrenSelector;
        }
    }
}
تا اینجا ساختار کدهای پشت صحنه‌ی DntTreeView.razor.cs مشخص شد. اکنون UI این کامپوننت را به صورت زیر تکمیل می‌کنیم:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

@if (Items is null || !Items.Any())
{
    @EmptyContentTemplate
}
else
{
    <CascadingValue Value="this">
        <ul @attributes="AdditionalAttributes">
            @foreach (var item in Items)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    </CascadingValue>
}
در ابتدای کار، اگر آیتمی برای نمایش وجود نداشته باشد، EmptyContentTemplate دریافتی از استفاده کننده را رندر می‌کنیم. در غیراینصورت، حلقه‌ای را بر روی لیست Items ایجاد کرده و آن‌‌ها را یکی نمایش می‌دهیم. این نمایش، نکات زیر را به همراه دارد:
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام می‌شود که آن‌‌هم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت می‌کند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده می‌کنید. این روش، یکی از روش‌های اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننت‌های فرزند است که در ادامه از آن استفاده خواهیم کرد.


تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor

اگر به حلقه‌ی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمی‌دهد؛ اما کامنت‌های ما چندسطحی و تو در تو هستند و عمق آن‌ها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننت‌های Blazor داشت که خوشبختانه این مورد پیش‌بینی شده‌است و هر کامپوننت Blazor، می‌تواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

<li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()">
    @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null)
    {
        @SafeOwnerTreeView.ItemTemplate(ParentItem)
    }
    @if (Children is not null)
    {
        <ul>
            @foreach (var item in Children)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    }
</li>
این‌ها کدهای DntTreeViewChildrenItem.razor هستند که در آن، ابتدا ItemTemplate دریافتی از والد یا همان DntTreeView.razor رندر می‌شود. سپس به کمک CompiledChildrenSelector ای که عنوان شد، یک شیء Children را تشکیل داده و آن‌را به خودش (فراخوانی مجدد DntTreeViewChildrenItem در اینجا)، ارسال می‌کند. این فراخوانی بازگشتی، سبب رندر تمام سطوح تو در توی شیء جاری می‌شود.

کدهای پشت صحنه‌ی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary>
///     A custom DntTreeView
/// </summary>
public partial class DntTreeViewChildrenItem<TRecord>
{
    /// <summary>
    ///     Defines the owner of this component.
    /// </summary>
    [CascadingParameter]
    public DntTreeView<TRecord>? OwnerTreeView { get; set; }

    private DntTreeView<TRecord> SafeOwnerTreeView =>
        OwnerTreeView ??
        throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`.");

    /// <summary>
    ///     Nested parent item to display
    /// </summary>
    [Parameter]
    public TRecord? ParentItem { set; get; }

    private IEnumerable<TRecord>? Children =>
        ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null
            ? null
            : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem);
}
با استفاده از یک پارامتر از نوع CascadingParameter، می‌توان به اطلاعات شیء CascadingValue ای که در کامپوننت والد DntTreeView.razor قرا دادیم، دسترسی پیدا کنیم. سپس یکبار هم بررسی می‌کنیم که آیا نال هست یا خیر. یعنی قرار نیست که این کامپوننت فرزند، درجائی به صورت مستقیم استفاده شود. فقط قرار است داخل کامپوننت والد فراخوانی شود. به همین جهت اگر این CascadingParameter نال بود، یعنی این کامپوننت فرزند، به اشتباه فراخوانی شده و با صدور استثنائی این مساله را گوشزد می‌کنیم. اکنون که به SafeOwnerTreeView یا همان نمونه‌ای از شیء والد دسترسی پیدا کردیم، می‌توانیم پارامتر CompiledChildrenSelector آن‌را نیز فراخوانی کرده و توسط آن، به شیء تو در توی جدیدی در صورت وجود، جهت رندر بازگشتی آن رسید.
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر می‌کند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آن‌را با فراخوانی مجدد خودش ، رندر می‌کند.


روش استفاده از کامپوننت DntTreeView

اکنون که کار توسعه‌ی کامپوننت جنریک DntTreeView پایان یافت، روش استفاده‌ی از آن به صورت زیر است:
<div class="card" dir="rtl">
    <div class="card-header">
        DntTreeView
    </div>
    <div class="card-body">
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
            style="list-style: none;"
            ChildrenHtmlAttributes="ChildrenHtmlAttributes">
            <ItemTemplate Context="record">
                <div class="card mb-1">
                    <div class="card-body">
                        <span>@record.Text</span>
                    </div>
                </div>
            </ItemTemplate>
            <EmptyContentTemplate>
                <div class="alert alert-warning">
                    There is no item to display!
                </div>
            </EmptyContentTemplate>
        </DntTreeView>
    </div>
</div>
همانطور که مشاهده می‌کنید، چون کامپوننت جنریک است، باید نوع TRecord را که در مثال ما، شیء Comment است، مشخص کرد. سپس لیست نظرات، خاصیت تو در تو، قالب سفارشی نمایش Text نظرات (با توجه به Context دریافتی که امکان دسترسی به شیء جاری در حال رندر را میسر می‌کند) و همچنین قالب سفارشی نبود اطلاعاتی برای نمایش را تعریف می‌کنیم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافته‌ی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار می‌کند.
مطالب
چگونگی رسیدگی به Null property در AutoMapper

AutoMapper کتابخانه‌ای برای نگاشت اطلاعات یک شیء به شی‌ءایی دیگر به صورت خودکار می‌باشد.

در این مقاله چگونگی رسیدگی به Null property را در AutoMapper   بررسی خواهیم کرد. فرض کنید شیء منبع دارای یک خاصیت Null  است و می‌خواهید به وسیله Automaper شیء منبع را به مقصد نگاشت نمایید. اما می‌خواهید در صورت Null بودن شیء مبدا، یک مقدار پیش فرض برای شیء مقصد در نظر گرفته شود .

برای نمونه کلاسuser   را که در آن از کلاس Address یک خاصیت تعریف شده، در نظر بگیرید. اگر مقدار آدرس در شیء منبع خالی بود شاید شما بخواهید مقدار آن را به صورت empty string و یا با یک مقدار پیش فرض در مقصد مقدار دهی کنید.

همانند مثال زیر : 

public class UserSource
{
  public Address Address{get;set;}
}
 
public class UserDestination
{
  public string Address{get;set;}
}
ابتدا نگاشت‌ها را تعریف می‌کنیم:
AutoMapper.Mapper.CreateMap<UserSource, UserDestination>()
          .ForMember(dest => dest.Address
          , opt => opt.NullSubstitute("Address not found")
          );
کد بالا نشان دهنده تبدیل Address به Address است ولی دارای متد اختیاری NullSubstitute می‌باشد و بیانگر این است که اگر آدرس شیء منبع Null بود، مقدار پیش فرضی را برای شیء مقصد در نظر بگیرد. در انتها  می‌توان نگاشت را در برنامه متناسب با نیاز خود انجام داد:
var model = AutoMapper.Mapper.Map<UserSource, UserDestination>(user);
var models = AutoMapper.Mapper.Map<IEnumerable<UserSource>, IEnumerable<UserDestination>>(users);
مطالب
سری بررسی SQL Smell در EF Core - ایجاد روابط Polymorphic - بخش دوم
در مطلب قبل نحوه‌ی ایجاد روابط Polymorphic را بررسی کردیم و همچنین چندین راه‌حل جایگزین را نیز ارائه دادیم. همانطور که عنوان شد این نوع روابط اساساً از لحاظ طراحی دیتابیس اصولی نیستند و تا حد امکان نباید استفاده شوند. این نوع روابط بیشتر ORM friendly هستند و اکثر فریم‌ورک‌های غیردات‌نتی به عنوان یک گزینه‌ی توکار، امکان ایجاد این روابط را فراهم میکنند. به عنوان مثال درLaravel Eloquent ORM به صورت توکار از این قابلیت پشتیبانی می‌شود: 
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Get the owning commentable model.
     */
    public function commentable()
    {
        return $this->morphTo();
    }
}

class Post extends Model
{
    /**
     * Get all of the post's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

class Video extends Model
{
    /**
     * Get all of the video's comments.
     */
    public function comments()
    {
        return $this->morphMany('App\Comment', 'commentable');
    }
}

اما در Entity Framework Core هنوز این قابلیت پیاده‌سازی نشده است. در اینجا می‌توانید وضعیت پیاده‌سازی Polymorphic Association را پیگیری کنید. پیاده‌سازی این قابلیت از این جهت مهم است که امکان کوئری نویسی را برای این نوع روابط ساده‌تر خواهد کرد به عنوان مثال در کدهای PHP فوق جهت واکشی کامنت‌های یک مطلب می‌توانیم اینگونه عمل کنیم:
$post = App\Post::find(1);

foreach ($post->comments as $comment) {
    //
}

همچنین امکان واکشی owner این رابطه را نیز حین کار با کامنت‌ها را خواهیم داشت:
$comment = App\Comment::find(1);

$commentable = $comment->commentable;

در ادامه می‌خواهیم معادل LINQ آن را پیاده‌سازی کنیم. در مطلب قبل مدل Comment این چنین ساختاری داشت:
public enum CommentType
{
    Article,
    Video,
    Event
}

public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }
}

در اینجا همانطور که مشاهده میکنید هیچگونه ارتباط معناداری بین Comment و همچنین owner رابطه (که ممکن است هر کدام از مقادیر Enum فوق باشد) وجود ندارد. اگر این مدل به تنها یک مدل مثلاً Article اشاره داشته باشد، نیاز به تعیین Navigation Property در دو طرف رابطه خواهد بود:
public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }

    public virtual Article Article { get; set; }
    public int ArticleId { get; set; }
}

public class Article
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    public virtual ICollection<Article> Articles { get; set; }
}

اما از آنجائیکه رابطه یک حالت پویا دارد، نمی‌توانیم به صورت صریح نوع ارجاعات را در دو طرف رابطه تعیین کنیم. برای داشتن همچین قابلیتی می‌توانیم Navigation Property را به صورت [NotMapped] تعیین کنیم که EF Core آنها را در نظر نگیرید. بنابراین به صورت دستی عملکرد آنها را پیاده‌سازی خواهیم کرد. برای اینکار می‌توانیم یک اینترفیس با عنوان ICommentable را تعریف کنیم و برای هر مدلی که نیاز به قابلیت کامنت دارد، این اینترفیس را پیاده‌سازی کنیم. همچنین یک ارجاع به لیستی از کامنت‌ها را به صورت Navigation Property به هر کدام از مدلها نیز اضافه خواهیم کرد:  
interface ICommentable
{
    int Id { get; set; }
}

public class Article : ICommentable
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Slug { get; set; }
    public string Description { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

public class Video : ICommentable
{
    public int Id { get; set; }
    public string Url { get; set; }
    public string Description { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

public class Event : ICommentable
{
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTimeOffset? Start { get; set; }
    public DateTimeOffset? End { get; set; }
    
    [NotMapped]
    public ICollection<Comment> Comments { get; set; }
}

سپس درون مدل Comment ارجاع به Polymorphic relation را نیز به صورت [NotMapped] پیاده‌سازی خواهیم کرد:
public class Comment
{
    public int Id { get; set; }
    public string CommentText { get; set; }
    public string User { get; set; }
    public int? TypeId { get; set; }
    public CommentType CommentType { get; set; }

    ICommentable _parent;

    [NotMapped]
    public ICommentable Parent
    {
        get => _parent;
        set
        {
            _parent = value;
            TypeId = value.Id;
            CommentType = (CommentType) Enum.Parse(typeof(CommentType), value.GetType().Name);
        }
    }
}

کاری که در بالا انجام شده، تنظیم تایپ مدلی است که می‌خواهیم واکشی کنیم. یعنی به محض مقداردهی، پراپرتی Comments مدل مورد نظر به همراه Id و در نهایت نوع آن را تنظیم کرده‌ایم. اکنون برای واکشی کامنت‌های یک مطلب خواهیم داشت:
var article = dbContext.Articles.Find(1);
            
article.Comments = dbContext.Comments
    .Where(c => c.TypeId == article.Id
                && c.CommentType == CommentType.Article)
    .ToList();

foreach (var comment in article.Comments)
    comment.Parent = article;

foreach (var comment in article.Comments)
{
    Console.WriteLine($"{comment.User} - ${comment.CommentText} - {((Article) comment.Parent).Title}");
}

همانطور که مشاهده می‌کنید اکنون می‌توانیم از هر دو طرف رابطه به اطلاعات موردنیازمان دسترسی داشته باشیم.
نظرات مطالب
غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن
یک نکته‌ی تکمیلی: پیاده سازی IXmlRepository مایکروسافت برای EF Core

از زمان ارائه‌ی NET Core 2.2.، بسته‌ی نیوگت جدید Microsoft.AspNetCore.DataProtection.EntityFrameworkCore ارائه شده‌است که کار آن دقیقا شبیه به پیاده سازی «یک نکته‌ی تکمیلی: روش ذخیره سازی کلید موقتی تولید شده در بانک اطلاعاتی بجای حافظه‌ی سرور» است که در نظرات فوق ارائه شد.
برای استفاده‌ی از آن، ابتدا بسته‌ی نیوگت آن‌را به برنامه اضافه کنید:
dotnet add package Microsoft.AspNetCore.DataProtection.EntityFrameworkCore

سپس Context ای را که بر اساس اینترفیس IDataProtectionKeyContext آن پیاده سازی شده‌است و دارای DbSet جدید از نوع DataProtectionKey است، تعریف کنید:
    public class MyKeysContext : DbContext, IDataProtectionKeyContext
    {
        // A recommended constructor overload when using EF Core 
        // with dependency injection.
        public MyKeysContext(DbContextOptions<MyKeysContext> options) 
            : base(options) { }

       // This maps to the table that stores keys.
        public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
    }
که با اجرای مهاجرت‌ها، یک جدول جدید را با سه فیلد زیر، ایجاد می‌کند:
public int Id { get; set; }
public string FriendlyName { get; set; }
public string XmlData { get; set; }

در آخر روش معرفی این Context به سیستم DataProtection به صورت زیر است:
public void ConfigureServices(IServiceCollection services)
{
   // using Microsoft.AspNetCore.DataProtection;
    services.AddDataProtection()
        .PersistKeysToDbContext<MyKeysContext>();
}
به این ترتیب، به صورت خودکار، اطلاعات موقتی کلیدهای رمزنگاری سیستم data-protection در بانک اطلاعاتی ذخیره شده و یا بازیابی می‌شوند.
نظرات مطالب
ASP.NET MVC #8
ناکافی توضیح داده شده. MvcHtmlString کارش «محصور سازی» یک رشته معمولی به صورت وهله‌ای از کلاس HtmlString است. چون محصور سازی شده، MSDN نوشته Html-encoded که باید می‌نوشت «Wraps the string in a class that implements IHtmlString»
ارائه یک رشته به صورت وهله‌ای از IHtmlString سبب خواهد شد تا زمانیکه از
<%: %>
و یا @ استفاده می‌شود، از حالت HTML encoded محافظت شود.
این سورس Html.Raw است:
public IHtmlString Raw(string value)
{
    return new HtmlString(value);
}

public IHtmlString Raw(object value)
{
    return new HtmlString(value == null ? null : value.ToString());
}
و این هم سورس MvcHtmlString :
namespace System.Web.Mvc
{
    public sealed class MvcHtmlString : HtmlString
    {
        [SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes", Justification = "MvcHtmlString is immutable")]
        public static readonly MvcHtmlString Empty = Create(String.Empty);

        private readonly string _value;

        public MvcHtmlString(string value)
            : base(value ?? String.Empty)
        {
            _value = value ?? String.Empty;
        }

        public static MvcHtmlString Create(string value)
        {
            return new MvcHtmlString(value);
        }

        public static bool IsNullOrEmpty(MvcHtmlString value)
        {
            return (value == null || value._value.Length == 0);
        }
    }
}
در اینجا داریم MvcHtmlString : HtmlString یعنی MvcHtmlString از جنس همان HtmlString ایی است که متدهای MvcHtmlString.Create و Html.Raw باز می‌گردانند.

کاربرد MvcHtmlString عموما در بازگرداندن مقادیر از helper methods است؛ زمانیکه می‌دانید خروجی آن دارای تگ‌های html است. مثلا قرار است یک دکمه را نمایش دهد. به این ترتیب خروجی متد از encode شدن خودکار محافظت می‌شود.
در MVC، سیستم خروجی، رشته‌هایی از جنس IHtmlString را encode نمی‌کند. این بیشتر یک قرار داد است در اینجا.
نظرات مطالب
RadioButtonList در ASP.NET MVC

من که کدی نمی‌بینم اینجا برای دیباگ و بررسی: آناتومی یک گزارش خطای خوب

ولی احتمالا نام خاصیت ViewModel شما با نام گروهی که تعریف کردید یکی نیست چون در مثال اول بین پارامتر string tech و کلاس زیر فرقی نیست:

public class SomeClass
{
  public string Tech {set; get;}
}
نظرات مطالب
چگونگی دسترسی به فیلد و خاصیت غیر عمومی
آیا می‌توان به کمک رفلکشن به خصوصیتی که مثلا Set ندارد مقدار دهی کرد. بعنوان مثال
class Program
{
  static void Main(string[] args)
  {
      Book book = new Book();
      var codeprop = book.GetType().GetProperty("Code", BindingFlags.Public | BindingFlags.Instance);
      codeprop.SetValue(book, 20, null);
      var value = codeprop.GetValue(book, null);
  }
}

public class Book
{
  public int code;
  public int Code
  {
      get { return code; }
  }
}
مطالب دوره‌ها
به روز رسانی خواص راهبری و مجموعه‌های Entity Framework توسط AutoMapper
فرض کنید مدل‌های بانک اطلاعاتی ما چنین ساختاری را دارند:
public abstract class BaseEntity
{
    public int Id { set; get; }
}

public class User : BaseEntity
{
        public string Name { set; get; }
 
        public virtual ICollection<Advertisement> Advertisements { get; set; }
}

public class Advertisement : BaseEntity
{
    public string Title { get; set; }
    public string Description { get; set; }
 
    [ForeignKey("UserId")]
    public virtual User User { get; set; }
    public int UserId { get; set; }
}
و همچنین مدل‌های رابط کاربری یا ViewModel‌های برنامه نیز به صورت ذیل تعریف شده‌اند:
public class AdvertisementViewModel
{
    public int Id { get; set; }
    public string Title { get; set; }
    public int UserId { get; set; }
}
 
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public List<AdvertisementViewModel> Advertisements { get; set; }
}


به روز رسانی خواص راهبری Entity framework توسط AutoMapper

در کلاس‌های فوق، یک کاربر، تعدادی تبلیغات را می‌تواند ثبت کند. در این حالت اگر بخواهیم خاصیت User کلاس Advertisement را توسط AutoMapper به روز کنیم، با رعایت دو نکته، اینکار به سادگی انجام خواهد شد:
الف) همانطور که در کلاس Advertisement جهت تعریف کلید خارجی مشخص است، UserId نیز علاوه بر User ذکر شده‌است. این مورد کار نگاشت UserId اطلاعات دریافتی از کاربر را ساده کرده و در این حالت نیازی به یافتن اصل User این UserId از بانک اطلاعاتی نخواهد بود.
ب) چون در اطلاعات دریافتی از کاربر تنها Id او را داریم و نه کل شیء مرتبط را، بنابراین باید به AutoMapper اعلام کنیم تا از این خاصیت صرفنظر کند که اینکار توسط متد Ignore به نحو ذیل قابل انجام است:
this.CreateMap<AdvertisementViewModel, Advertisement>()
      .ForMember(advertisement => advertisement.Description, opt => opt.Ignore())
      .ForMember(advertisement => advertisement.User, opt => opt.Ignore());


به روز رسانی مجموعه‌های Entity Framework توسط AutoMapper

فرض کنید چنین اطلاعاتی از کاربر و رابط کاربری برنامه دریافت شده است:
var uiUser1 = new UserViewModel
{
    Id = 1,
    Name = "user 1",
    Advertisements = new List<AdvertisementViewModel>
    {
        new AdvertisementViewModel
        {
            Id = 1,
            Title = "Adv 1",
            UserId = 1
        },
        new AdvertisementViewModel
        {
            Id = 2,
            Title = "Adv 2",
            UserId = 1
        }
    }
};
اکنون می‌خواهیم معادل این رکورد را از بانک اطلاعاتی یافته و سپس اطلاعات آن‌را بر اساس اطلاعات UI به روز کنیم. شاید در نگاه اول چنین روشی پیشنهاد شود:
 var dbUser1 = ctx.Users.Include(user => user.Advertisements).First(x => x.Id == uiUser1.Id);
Mapper.Map(source: uiUser1, destination: dbUser1);
ابتدا کاربری را که Id آن مساوی uiUser1.Id است، یافته و سپس به AutoMapper اعلام می‌کنیم تا تمام اطلاعات آن‌را به صورت یکجا به روز کند. این نگاشت را نیز برای آن تعریف خواهیم کرد:
 this.CreateMap<UserViewModel, User>()
در یک چنین حالتی، ابتدا شیء user 1 از بانک اطلاعاتی دریافت شده (و با توجه به وجود Include، تمام تبلیغات او نیز دریافت می‌شوند)، سپس ... دو رکورد دریافتی از کاربر، کاملا جایگزین اطلاعات موجود می‌شوند. این جایگزینی سبب تخریب پروکسی‌های EF می‌گردند. برای مثال اگر پیشتر تبلیغی با Id=1 در بانک اطلاعاتی وجود داشته، اکنون با نمونه‌ی جدیدی جایگزین می‌شود که سیستم Tracking و ردیابی EF اطلاعاتی در مورد آن ندارد. به همین جهت اگر در این حالت ctx.SaveChanges فراخوانی شود، عملیات ثبت و یا به روز رسانی با شکست مواجه خواهد شد.

علت را در این دو تصویر بهتر می‌توان مشاهده کرد:



تصویر اول که مستقیما از بانک اطلاعاتی حاصل شده‌است، دارای پروکسی‌های EF است. اما در تصویر دوم، جایگزین شدن این پروکسی‌ها را مشاهده می‌کنید که سبب خواهد شد این اشیاء دیگر تحت نظارت EF نباشند.


راه حل:

در این مورد خاص باید به AutoMapper اعلام کنیم تا کاری با لیست تبلیغات کاربر دریافت شده‌ی از بانک اطلاعاتی نداشته باشد و آن‌را راسا جایگزین نکند:
this.CreateMap<UserViewModel, User>().ForMember(user => user.Advertisements, opt => opt.Ignore());
در اینجا متد Ignore را بر روی لیست تبلیغات کاربر بانک اطلاعاتی فراخوانی کرده‌ایم، تا اطلاعات آن پس از اولین نگاشت انجام شده‌ی توسط AutoMapper دست نخورده باقی بماند.
سپس کار ثبت یا به روز رسانی را به صورت نیمه خودکار مدیریت می‌کنیم:
using (var ctx = new MyContext())
{
    var dbUser1 = ctx.Users.Include(user => user.Advertisements).First(x => x.Id == uiUser1.Id);
    Mapper.Map(source: uiUser1, destination: dbUser1);
 
    foreach (var uiUserAdvertisement in uiUser1.Advertisements)
    {
        var dbUserAdvertisement = dbUser1.Advertisements.FirstOrDefault(ad => ad.Id == uiUserAdvertisement.Id);
        if (dbUserAdvertisement == null)
        {
            // Add new record
            var advertisement = Mapper.Map<AdvertisementViewModel, Advertisement>(uiUserAdvertisement);
            dbUser1.Advertisements.Add(advertisement);
        }
        else
        {
            // Update the existing record
            Mapper.Map(uiUserAdvertisement, dbUserAdvertisement);
        }
    }
 
    ctx.SaveChanges();
}
- در اینجا ابتدا db user معادل اطلاعات ui user از بانک اطلاعاتی، به همراه لیست تبلیغات او دریافت می‌شود و اطلاعات ابتدایی او نگاشت خواهند شد.
- سپس بر روی اطلاعات تبلیغات دریافتی از کاربر، یک حلقه را تشکیل خواهیم داد. در اینجا هربار بررسی می‌کنیم که آیا معادل این تبلیغ هم اکنون به شیء db user متصل است یا خیر؟ اگر متصل نبود، یعنی یک رکورد جدید است و باید Add شود. اگر متصل بود صرفا باید به روز رسانی صورت گیرد.
- برای حالت ایجاد شیء جدید بانک اطلاعاتی، بر اساس uiUserAdvertisement دریافتی، می‌توان از متد Mapper.Map استفاده کرد؛ خروجی این متد، یک شیء جدید تبلیغ است.
- برای حالت به روز رسانی اطلاعات db user موجود، بر اساس اطلاعات ارسالی کاربر نیز می‌توان از متد Mapper.Map کمک گرفت.


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


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample04.zip
مطالب دوره‌ها
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
فرض کنید تعدادی ردیف در گزارشی نمایش داده شده‌اند. قصد داریم برای هر ردیف یک دکمه حذف را قرار دهیم. این حذف باید Ajax ایی باشد؛ به علاوه در حین حذف ردیف، پویانمایی محو آن ردیف را نیز سبب شود.


مدل و منبع داده برنامه

namespace jQueryMvcSample06.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}

using System.Collections.Generic;
using jQueryMvcSample06.Models;

namespace jQueryMvcSample06.DataSource
{
    /// <summary>
    /// منبع داده فرضی جهت سهولت دموی برنامه
    /// </summary>
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        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;
        }

        public static IList<BlogPost> LatestBlogPosts
        {
            get { return _cachedItems; }
        }
    }
}
در اینجا مدل برنامه که ساختار نمایش یک سری مطلب را تهیه می‌کند، ملاحظه می‌کنید؛ به علاوه یک منبع داده فرضی تشکیل شده در حافظه جهت سهولت دموی برنامه.


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

using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample06.DataSource;
using jQueryMvcSample06.Security;

namespace jQueryMvcSample06.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var postsList = BlogPostDataSource.LatestBlogPosts;
            return View(postsList);
        }

        [AjaxOnly]
        [HttpPost]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult DeleteRow(int? postId)
        {
            if (postId == null)
                return Content(null);

            //todo: delete post from db

            return Content("ok");
        }
    }
}
کنترلر برنامه بسیار ساده بوده و نکته خاصی ندارد. در حین اولین بار نمایش صفحه، لیست مطالب را به View مرتبط ارسال می‌کند. همچنین یک اکشن متد حذف ردیف‌های نمایش داده شده را نیز در اینجا تدارک دیده‌ایم. این اکشن متد از طریق ارسال اطلاعات به صورت Ajax، شماره مطلب را در اختیار برنامه قرار می‌دهد که توسط آن در ادامه برای مثال می‌توان این رکورد را از بانک اطلاعاتی حذف کرد. امضای متد DeleteRow بر اساس پارامترهای ارسالی توسط jQuery Ajax مشخص و تنظیم شده‌اند:
 data: JSON.stringify({ postId: postId }),

View برنامه

@model IEnumerable<jQueryMvcSample06.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var postUrl = Url.Action(actionName: "DeleteRow", controllerName: "Home");
}
<h2>
    حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن</h2>
<table>
    <tr>
        <th>
            عملیات
        </th>
        <th>
            عنوان
        </th>
    </tr>
    @foreach (var item in Model)
    {
        <tr>
            <td>
                <span id="row-@item.Id">حذف</span>
            </td>
            <td>
                @item.Title
            </td>
        </tr>
    }
</table>
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $('span[id^="row"]').click(function () {
                var span = $(this);
                var postId = span.attr('id').replace('row-', '');
                var tableRow = span.parent().parent();
                $.ajax({
                    type: "POST",
                    url: '@postUrl',
                    data: JSON.stringify({ postId: postId }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        }
                        else if (status === 'error' || !data || data == "nok") {
                            alert('خطایی رخ داده است');
                        }
                        else {
                            $(tableRow).fadeTo(600, 0, function () {
                                $(tableRow).remove();
                            });
                        }
                    }
                });
            });
        });
    </script>
}
کدهای View برنامه را در ادامه ملاحظه می‌کنید. اطلاعات مطالب دریافتی به صورت یک جدول در صفحه نمایش داده شده‌اند. در هر ردیف توسط یک span که با css تزئین گردیده است، یک دکمه حذف را تدارک دیده‌ایم. برای اینکه در حین کار با jQuery بتوانیم id هر ردیف را بدست بیاوریم، این id را در قسمتی از id این span اضافه شده قرار داده‌ایم.
در کدهای اسکریپتی صفحه، ابتدا کلیک بر روی کلیه spanهایی که id آن‌ها با row شروع می‌شود را مونیتور خواهیم کرد:
 $('span[id^="row"]').click(function () {
سپس هر زمان که بر روی یکی از این spanها کلیک شد، می‌توان بر اساس span جاری، id و همچنین tableRow مرتبط را استخراج کرد:
 var span = $(this);
var postId = span.attr('id').replace('row-', '');
var tableRow = span.parent().parent();
اکنون که به این اطلاعات دسترسی پیدا کرده‌ایم، تنها کافی است آن‌ها را توسط متد ajax به کنترلر برنامه برای پردازش نهایی ارسال نمائیم. همچنین در پایان کار عملیات، توسط متدهای fadeTo و remove ایی که ملاحظه می‌کنید، سبب حذف نمایشی یک ردیف به همراه پویانمایی محو آن خواهیم شد.


دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample06.zip 
نظرات مطالب
ASP.NET MVC #10
با سلام
آیا mvc در binding نوع داده decimal مشکلی دارد؟
من یک مدل مانند زیر ساخته ام
public class TestDecimal
    {
        public string TestName { get; set; }
        public int TestInt { get; set; }
        public decimal TestDecimal1 { get; set; }
        public decimal TestDecimal2 { get; set; }
        public decimal? TestDecimal3 { get; set; }

    }
حالا کنترلر رو کامل میکنم
public ActionResult test()
        {
            var model = new TestDecimal();
            return View(model);
        }

        [HttpPost]
        public ActionResult test(TestDecimal model)
        {
            return View(model);
        }
و در آخر view
@model Test.Models.TestDecimal

<h2>test decimal</h2>
@using (Ajax.BeginForm(
    actionName: "test",
    controllerName: "DocRate",
    ajaxOptions: new AjaxOptions
    {
        HttpMethod = "POST",
        InsertionMode = InsertionMode.Replace
    }))
{
    <div dir="ltr">
        @Html.TextBoxFor(m => m.TestName)
        @Html.TextBoxFor(m => m.TestInt)
        @Html.TextBoxFor(m => m.TestDecimal1)
        @Html.TextBoxFor(m => m.TestDecimal2)
        @Html.TextBoxFor(m => m.TestDecimal3)

        <br/>
        <input id="submitRate" type="submit" value=" ثبت امتیاز"/>
    </div>
}
نتیجه نهایی

 جالبه؟ با اینکه فیلدهای decimal پر شده ولی نتیجه bind نمیشه
همین فیلدهای decimal رو اگر با اعداد صحیح نه اعشاری پر کنم binding انجام میشود!
مشکل از کجا است؟