مطالب
استفاده از Fluent Validation در برنامه‌های ASP.NET Core - قسمت چهارم - اعتبارسنجی Async سمت کلاینت و یا همان Remote Client Side Validation
در قسمت قبل با نحوه‌ی پیاده سازی اعتبارسنجی‌های سفارشی سمت کلاینت مخصوص کتابخانه‌ی Fluent Validation آشنا شدیم. در این قسمت، یک حالت خاص همان نوع اعتبارسنجی‌های سمت کلاینت را که remote validation نام دارد، بررسی می‌کنیم. در این حالت خاص، نیازی به کدنویسی جاوااسکریپتی خاصی نیست. چون زیرساخت آن به همراه unobtrusive jQuery Ajax خود ASP.NET Core ارائه می‌شود. در اینجا فقط نیاز است تا متادیتای خاص آن‌را تولید کنیم. به عبارتی اینبار هدف ما تنها تولید یک چنین تگ HTML ای است:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val ، data-val-remote و data-val-remote-url است. همینقدر که این سه ویژگی وجود داشته باشند، مابقی منطق اعتبارسنجی سمت کلاینت آن توسط unobtrusive jQuery Ajax (ارسال خودکار Ajax ای مقدار ایمیل، به سمت سرور و دریافت پاسخ) و unobtrusive java script validation مدیریت خواهند شد.


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

به همان مدل قسمت قبل، قصد داریم خاصیت آدرس ایمیل را هم اضافه کنیم:
using System.ComponentModel.DataAnnotations;

namespace FluentValidationSample.Models
{
    public class UserModel
    {
        [Display(Name = "نام کاربری")]
        public string Username { get; set; }

        [Display(Name = "سن")]
        public int Age { get; set; }

        [Display(Name = "سابقه کار")]
        public int Experience { get; set; }

        [DataType(DataType.EmailAddress)]
        [Display(Name = "آدرس ایمیل")]
        public string Email { get; set; }
    }
}


ایجاد سرویسی برای بررسی منحصربفرد بودن آدرس ایمیل

در ادامه قصد داریم سرویسی را ایجاد کنیم که برای مثال با بانک اطلاعاتی ارتباط برقرار کرده (در اینجا جهت سهولت ارائه، از یک آرایه استفاده شده‌است) و مشخص می‌کند که آیا ایمیل دریافتی پیشتر استفاده شده‌است یا خیر:
using System.Linq;

namespace FluentValidationSample.Services
{
    public interface IUsersService
    {
        bool IsUniqueEmail(string emailAddress);
    }

    public class UsersService : IUsersService
    {
        public bool IsUniqueEmail(string emailAddress)
        {
            string[] registedEmails = {
                "email@site.com",
                "test@gmail.com"
            };
            return !registedEmails.Contains(emailAddress);
        }
    }
}
این سرویس را هم با طول عمر Scoped به برنامه معرفی می‌کنیم؛ چون طول عمر سرویس‌هایی که با بانک اطلاعاتی و DbContext کار می‌کنند، به همین نحو تعیین می‌شوند:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();


ایجاد اعتبارسنج سمت سرور بررسی منحصربفرد بودن آدرس ایمیل

در ادامه یک PropertyValidator جدید را ایجاد می‌کنیم تا بتوان توسط آن مقدار ایمیل دریافتی را در سمت سرور، تعیین اعتبار کرد:
using FluentValidation.Validators;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UniqueEmailValidator : PropertyValidator
    {
        private readonly IUsersService _usersService;

        public UniqueEmailValidator(IUsersService usersService)
                : base("این آدرس ایمیل هم اکنون مورد استفاده‌است")
        {
            _usersService = usersService;
        }

        protected override bool IsValid(PropertyValidatorContext context)
        {
            return context.PropertyValue != null &&
                    _usersService.IsUniqueEmail((string)context.PropertyValue);
        }
    }
}
همانطور که مشاهده می‌کنید، در اینجا تزریق سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج، مجاز است و بدون مشکل کار می‌کند.
پس از آن جهت سهولت استفاده‌ی از آن، یک متد الحاقی جدید را نیز به نام UniqueEmail به نحو زیر تعریف می‌کنیم:
using FluentValidation;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public static class CustomValidatorExtensions
    {
        public static IRuleBuilderOptions<T, string> UniqueEmail<T>(
            this IRuleBuilder<T, string> ruleBuilder, IUsersService usersService)
        {
            return ruleBuilder.SetValidator(new UniqueEmailValidator(usersService));
        }
    }
}

یک نکته: اگر دقت کرده باشید، فضای نام این اعتبارسنج در این قسمت FluentValidationSample.ModelsValidations شده‌است:


علت اینجا است که اعتبارسنج تعریف شده نیاز دارد هم از مدل‌ها استفاده کند و هم از سرویس کاربران. سرویس کاربران هم از مدل‌ها استفاده می‌کند. به همین جهت اگر تعاریف اعتبارسنجی را داخل پروژه‌ی مدل‌ها قرار دهیم، یک وابستگی حلقوی رخ خواهد داد (وابستگی مدل‌ها به سرویس‌ها و برعکس). بنابراین بهتر است اعتبارسنج‌ها را به یک پروژه‌ی مجزا منتقل کنیم تا از بروز این cyclic dependency جلوگیری شود.


اعمال اعتبارسنجی منحصربفرد بودن ایمیل دریافتی به اعتبارسنج UserModel

پس از تهیه‌ی متد الحاقی UniqueEmail، آن‌را به RuleFor مخصوص خاصیت ایمیل اضافه می‌کنیم. در اینجا نیز تزریق وابستگی سرویس سفارشی IUsersService به سازنده‌ی کلاس اعتبارسنج مجاز است:
using FluentValidation;
using FluentValidationSample.Models;
using FluentValidationSample.Services;

namespace FluentValidationSample.ModelsValidations
{
    public class UserModelValidator : AbstractValidator<UserModel>
    {
        public UserModelValidator(IUsersService usersService)
        {
            RuleFor(x => x.Username).NotNull();
            RuleFor(x => x.Age).NotNull();
            RuleFor(x => x.Experience).LowerThan(nameof(UserModel.Age)).NotNull();
            RuleFor(x => x.Email).EmailAddress().NotNull().UniqueEmail(usersService);
        }
    }
}


ایجاد متادیتای مورد نیاز جهت unobtrusive java script validation در سمت سرور

در ادامه نیاز است ویژگی‌های data-val خاص unobtrusive java script validation را توسط FluentValidation ایجاد کنیم:
using FluentValidation;
using FluentValidation.AspNetCore;
using FluentValidation.Internal;
using FluentValidation.Resources;
using FluentValidation.Validators;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace FluentValidationSample.ModelsValidations
{
    public class RemoteClientValidator : ClientValidatorBase
    {
        public string RemoteUrl { set; get; }

        public RemoteClientValidator(PropertyRule rule, IPropertyValidator validator) :
            base(rule, validator)
        {
        }

        public override void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-remote", GetErrorMessage(context));
            MergeAttribute(context.Attributes, "data-val-remote-url", RemoteUrl);
        }

        private string GetErrorMessage(ClientModelValidationContext context)
        {
            var formatter = ValidatorOptions.MessageFormatterFactory().AppendPropertyName(Rule.GetDisplayName());
            string messageTemplate;
            try
            {
                messageTemplate = Validator.Options.ErrorMessageSource.GetString(null);
            }
            catch (FluentValidationMessageFormatException)
            {
                messageTemplate = ValidatorOptions.LanguageManager.GetStringForValidator<NotEmptyValidator>();
            }
            return formatter.BuildMessage(messageTemplate);
        }
    }
}
در این کدها، تنها قسمت مهم آن، متد AddValidation است که کار تعریف و افزودن متادیتاهای unobtrusive java script validation را انجام می‌دهد و برای مثال سبب رندر تگ HTML ای زیر می‌شود:
<input dir="ltr" class="form-control input-validation-error" 
type="email" 
data-val="true" 
data-val-email="'آدرس ایمیل' is not a valid email address." 
data-val-remote="این آدرس ایمیل هم اکنون مورد استفاده‌است" 
data-val-remote-url="/Home/ValidateUniqueEmail" 
data-val-required="'آدرس ایمیل' must not be empty." 
id="Email" 
name="Email" 
>
که به همراه ویژگی‌های data-val، data-val-remote و data-val-remote-url است تا unobtrusive jQuery Ajax validation را فعال کند.


افزودن اعتبارسنج‌های تعریف شده به تنظیمات برنامه

پس از تعریف UniqueEmailValidator و RemoteClientValidator، روش افزودن آن‌ها به تنظیمات FluentValidation به صورت زیر است:
namespace FluentValidationSample.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUsersService, UsersService>();

            services.AddControllersWithViews().AddFluentValidation(
                fv =>
                {
                    fv.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
                    fv.RegisterValidatorsFromAssemblyContaining<RegisterModelValidator>();
                    fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;

                    fv.ConfigureClientsideValidation(clientSideValidation =>
                    {
                        // ...

                        clientSideValidation.Add(
                            validatorType: typeof(UniqueEmailValidator),
                            factory: (context, rule, validator) =>
                                        new RemoteClientValidator(rule, validator)
                                        {
                                            RemoteUrl = "/Home/ValidateUniqueEmail"
                                        });
                    });
                }
            );
        }
در اینجا یک RemoteUrl را هم مشاهده می‌کنید که به صورت زیر باید تعریف شود:
namespace FluentValidationSample.Web.Controllers
{
    public class HomeController : Controller
    {
        private readonly IUsersService _usersService;

        public HomeController(IUsersService usersService)
        {
            _usersService = usersService;
        }

         // ...

        public IActionResult ValidateUniqueEmail(string email)
        {
            return Ok(_usersService.IsUniqueEmail(email));
        }
    }
}
زمانیکه اعتبارسنجی سمت کلاینت رخ می‌دهد، آدرس ایمیل، به اکشن متد فوق ارسال شده و یک true و یا false را دریافت می‌کند که بیانگر موفقیت آمیز بودن و یا شکست اعتبارسنجی از راه دور است.


تعریف کدهای جاوا اسکریپتی مورد نیاز

پیش از هرکاری، اسکریپت‌های فایل layout برنامه باید چنین تعریفی را داشته باشند:
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
در اینجا مداخل jquery، سپس jquery.validate و بعد از آن jquery.validate.unobtrusive را مشاهده می‌کنید. در ادامه نیازی به تکمیل فایل js/site.js جهت افزودن کدهای remote client validation نیست و این کدها جزئی از کتابخانه‌ی jquery.validate.unobtrusive هستند.


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

View این قسمت نیز همانند قسمت قبل است که فقط یک آدرس ایمیل به آن اضافه شده‌است:


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


کدهای کامل این سری را تا این قسمت از اینجا می‌توانید دریافت کنید: FluentValidationSample-part04.zip
مطالب
Persist ، Load و Bookmark در Workflow

در خیلی از مواقع workflow‌ها به مرحله‌ای می‌رسند که احتیاج به دستوری از بیرون از فرآیند دارند. در هنگام انتظار، اگر به هر دلیلی workflow از حافظه حذف شود، امکان ادامه فرآیند وجود ندارد. اما می‌توان با Persist (ذخیره) کردن آن، در زمان انتظار و فراخوانی مجدد آن در هنگام نیاز، این ریسک را برطرف نمود.

قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.

ساختار اینترفیس کاربری ما WPF می‌باشد. پس در ابتدا یک پروژه از نوع WPF ایجاد می‌کنیم. اسم solution  را PersistWF و اسم Project را PersistWF.UI انتخاب می‌کنیم.

در پروژه  UI نام فایل MainWindow.xaml  را به AddRequest.xaml تغییر می‌دهیم. همچنین اسم کلاس مربوطه را در codebehind 

همین طور مقدار StartupUri را هم در app.xaml اصلاح می‌کنیم

StartupUri="AddRequest.xaml"

Reference ‌های زیر رو هم به پروژه اضافه می‌کنیم 

•System.Activities
•System.Activities.DurableInstancing
•System.Configuration
•System.Data.Linq
•System.Runtime.DurableInstancing
•System.ServiceModel
•System.ServiceModel.Activities
•System.Workflow.ComponentModel
•System.Runtime.DurableInstancing
•System.Activities.DurableInstancing

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

برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore   استفاده می‌کنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپت‌های مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نام‌های SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.

من یک دیتابیس با نام PersistWF ایجاد می‌کنم و اسکریپت‌ها را بر روی آن اجرا می‌کنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد می‌کنم.

و شمایل دیتابیس ما پس از اجرا کردن اسکریپت‌ها و ساختن جدول User  بدین شکل است: 

XAML زیر، ساختار فرم AddRequest می‌باشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAML‌های پیش فرض عوض کنید. 

<Window x:Class="PersistWF.UI.AddRequest"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded">
    <Grid MinWidth="300" MinHeight="100" Width="514">
        <Label Height="30" Margin="5,10,10,10" Name="lblName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Name:</Label>
        <Label Height="30" Margin="270,10,10,10" Name="lblPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Phone Number:</Label>
        <Label Height="30" Margin="5,40,10,10" Name="lblEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="90"  HorizontalContentAlignment="Right">Email:</Label>
        <TextBox Height="25" Margin="100,10,10,10" Name="txtName"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" />
        <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" />
        <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" />
        <Button Height="23" Margin="100,86,0,0" Name="brnRegister"  VerticalAlignment="Top" HorizontalAlignment="Left" Width="70"  Click="brnRegister_Click">Register</Button>
        <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145"  VerticalAlignment="Top" ItemsSource="{Binding}"  HorizontalContentAlignment="Center"  SelectionChanged="lstUsers_SelectionChanged" >
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Current User" Width="480">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding Name}"  Width="110"/>
                                    <TextBlock Text="{Binding Phone}"  Width="70"/>
                                    <TextBlock Text="{Binding Email}"  Width="130"/>
                                    <TextBlock Text="{Binding Status}"  Width="70"/>
                                    <TextBlock Text="{Binding AcceptedBy}"  Width="100"/>
                                </StackPanel>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>
        <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10"  Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" />
        <Label Height="30" Margin="10,0,0,140" Name="lblAgent"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40"  HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label>
        <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190"  Visibility="Hidden" />
        <Button Height="25" Margin="270,0,0,140" Name="btnAccept"  VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90"  Click="btnAccept_Click" Visibility="Hidden">Accept</Button>
        <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110"  Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label>
        <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100"  VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" />
    </Grid>
</Window>

اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل باشد 

اکثر workflow‌ها از activity معروف  WrteLine استفاده می‌کنند که برای نمایش یک رشته به کار می‌رود. ما هم در workflow مثالمان از این Activity استفاده می‌کنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد می‌شوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد می‌کنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.

کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک  Property استاتیک از جنس فرم AddRequest هم برای آن تعریف می‌کنیم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
    public static class ApplicationInterface
    {
        public static AddRequest _app { get; set; }
    }
}

به Constructor کلاس موجود در فایل AddRequest.xaml.cs  این خط کد رو اضافه می‌کنم

        public AddRequest()
        {
            InitializeComponent();
            ApplicationInterface._app = this;
        }
این دو متد را هم به این کلاس اضافه می‌کنیم  
private void AddEvent(string szText)
        {
            lstEvents.Items.Add(szText);
        }
        public ListBox GetEventListBox()
        {
            return this.lstEvents;
        }

متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کننده‌اش قرار می‌دهد.

و حالا کلاس TextWriter سفارشی‌امان را می‌نویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه می‌کنیم که از TextWriter مشتق می‌شود و محتویات آن‌را در زیر می‌بینید: 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace PersistWF.UI
{
    public class ListBoxTextWriter : TextWriter 
    { 
         const string textClosed = "This TextWriter must be opened before use"; 
         private Encoding _encoding; 
         private bool _isOpen = false; 
         private ListBox _listBox; 
         public ListBoxTextWriter() 
         { 
             // Get the static list box 
             _listBox = ApplicationInterface._app.GetEventListBox(); 
             if (_listBox != null) 
             _isOpen = true; 
         } 
         public ListBoxTextWriter(ListBox listBox) 
         { 
             this._listBox = listBox; 
             this._isOpen = true; 
         } 
         public override Encoding Encoding 
         { 
             get 
             { 
                if (_encoding == null) 
                { 
                    _encoding = new UnicodeEncoding(false, false); 
                } 
                return _encoding; 
             } 
         }
         public override void Close()
         {
             this.Dispose(true);
         }
         protected override void Dispose(bool disposing)
         {
             this._isOpen = false;
             base.Dispose(disposing);
         }
         public override void Write(char value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString())));
         }
         public override void Write(string value)
         {
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); 
             if (value != null)
                 this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value)));
         }
         public override void Write(char[] buffer, int index, int count)
         {
             String toAdd = "";
             if (!this._isOpen)
                 throw new ApplicationException(textClosed); ;
             if (buffer == null || index < 0 || count < 0)
                 throw new ArgumentOutOfRangeException("buffer");
             if ((buffer.Length - index) < count)
                 throw new ArgumentException("The buffer is too small");
             for (int i = 0; i < count; i++)
                 toAdd += buffer[i];
             this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd)));
         }
    }
}

همان طور که می‌بینید کلاس ListBoxTextWriter از کلاس abstract  TextWriter  مشتق شده و پیاده سازی از متد Write را فراهم می‌کند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متد‌ها را Override می‌کنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکتر‌ها را به ListBox اضافه کنیم) در constructor  پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر می‌سازد حتی وقتی‌که از یک thread متفاوت فراخوانی می‌شود، کار کند.

حالا می‌توانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.

به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم 

public static void AddEvent(String status)
        {
            if (_app != null)
            {
                new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status);
            }
        }

این هم از constructor دومی استفاده می‌کنه برای معرفی ListBox.

برای ارتباط با دیتابیس از LINQ to SQL استفاده می‌کنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه می‌کنیم. به دیتابیس متصل شده و جدول User رو به محیط Design می‌کشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده می‌کنیم. 

برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه می‌کنم تا کلاس‌های مورد نظر را آن‌جا ایجاد کنیم.

1. یک Activity برای ایجاد User

این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آن‌ها یک Instance از کلاس User ایجاد می‌کند و در حقیقت آن را به دیتابیس می‌فرستد و دخیره می‌کند. Connectionstring را هم می‌شود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر می‌گیریم تا User ایجاد شده را برگردانیم. روی پوشه‌ی Activities کلیک راست می‌کنیم و Add - NewItem را انتخاب می‌کنیم. از لیست workflow‌ها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد می‌کنیم 

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;

namespace PersistWF.UI.Activities
{

    public sealed class CreateUser : CodeActivity
    {


        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }

        public OutArgument<User> User { get; set; }

        protected override void Execute(CodeActivityContext context)
        {
            // ایجاد کاربر
            User user = new User();
            user.Email = Email.Get(context);
            user.Name = Name.Get(context);
            user.Phone = Phone.Get(context);
            user.Status = "New";
     user.WorkflowID = context.WorkflowInstanceId;
            UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context));
            db.Users.InsertOnSubmit(user);
            db.SubmitChanges();
            User.Set(context, user);
        }
    }
}

متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد می‌کند و به کمک DataContext آن‌را در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار می‌دهد.

1. یک Activity برای انتظار دریافت تایید

این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین قسمت صادر نماید.

این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافی‌است خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.

مثل قسمت قبل یک CodeActivity ایجاد می‌کنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر می‌دهیم. 

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Workflow.ComponentModel;

namespace PersistWF.UI.Activities
{

    public sealed class WaitForAccept<T> : NativeActivity<T>
    {
        public WaitForAccept()
            :base()
        {

        }
        public string BookmarkName { get; set; }
        public OutArgument<T> Input { get; set; } 

        protected override void Execute(NativeActivityContext context)
        {
            context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue));
        }

        private void Continue(NativeActivityContext context, Bookmark bookmark, object value)
        {
            Input.Set(context, (T)value); 
        }
        protected override bool CanInduceIdle
        {
            get
            {
                return true;
            }
        }
    }
}
این کلاس را generic نوشتم تا به جای User بشود هر پارامتر دیگه‌ای را به آن ارسال کرد. در واقع وقتی workflow به این Activity می‌رسد، Idle می‌شود. این activity  یک bookmark هم ایجاد می‌کند. ما وقتی workflow را با این bookmark فراخوانی کنیم؛ workflow از همینجا ادامه می‌یابد. فراخوانیbookmark می‌تواند همراه با وارد کردن یک  object باشد. متد Continue آن object را به آرگومان خروجی می‌دهد تا مسیر workflow را طی کند.
ما User  هایی را که به این نقطه رسیدنْ نمایش می‌دهیم. مدیر اونها را دیده و با مقدار دهی فیلد AcceptedBy، آن User را از اینجا به workflow می‌فرستد و ما user وارد شده را در ادامه‌ی فرآیند Accept می‌کنیم.
 
برای ایجاد workflow هم می‌توانید از designer استفاده کنید و هم می‌توانید کد مربوط به workflow را پیاده سازی کنید.

برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد می‌کنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:

using PersistWF.UI.Activities;
using System;
using System.Activities;
using System.Activities.Statements;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PersistWF.UI
{
     public sealed class UserWF : Activity
    {
        public InArgument<string> Name { get; set; }
        public InArgument<string> Email { get; set; }
        public InArgument<string> Phone { get; set; }
        public InArgument<string> ConnectionString { get; set; }
        public InArgument<TextWriter> Writer { get; set; }

        public UserWF()
        {
            Variable<User> User = new Variable<User> { Name = "User" };
            this.Implementation = () => new Sequence
            {
                DisplayName = "EnterUser",
                Variables = { User },
                Activities = { 
                     new CreateUser   //  1. ایجاد کاربر با ورود پارامتر‌های ورودی  
                    {
                        ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)),
                        Email = new InArgument<string>(c=> Email.Get(c)),
                        Name = new InArgument<string>(c=> Name.Get(c)),
                        Phone = new InArgument<string>(c=> Phone.Get(c)),
                        User = new OutArgument<User>(c=> User.Get(c))
                    },
                    new WriteLine // 2. لاگ مربوط به دخیره کاربر
                    {
                        TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                        Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) )
                    },
                    new InvokeMethod 
                     { 
                         TargetType = typeof(ApplicationInterface),  // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم
                         MethodName = "NewUser", 
                         Parameters = 
                         { 
                            new InArgument<User>(env => User.Get(env)) 
                         } 
                     }, 
                     new WaitForAccept<User>  // 4. اینجا فرایند متوقف می‌شود و منتظر تایید مدیر می‌ماند
                     {  
                        BookmarkName = "GetAcceptes",
                        Input = new OutArgument<User>(env => User.Get(env))
                     },
                     new WriteLine // 5. لاگ مربوط به تایید شدن کاربر
                     {
                         TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)),
                         Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy))
                     }
                }

            };

        }

    }
}

اگر بخوایم از Designer استفاده کنیم.  فرایندمان چیزی شبیه شکل زیر خواهد بود 

به Application بر می‌گردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring  رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است. 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
    </configSections>
    <connectionStrings>
        <add name="UserGenerator"
            connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
</configuration>

در کلاس AddRequest کد زیر را اضافه می‌کنم. برای نگهداری مقدار connectionstring 

private string _connectionString = "";

همچنین کد‌های زیر را به رویداد Load فرم اضافه می‌کنم تا مقدار ConnectionString را از Config بخوانم: 

Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
            ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings");
            _connectionString =  css.ConnectionStrings["UserGenerator"].ConnectionString;

خط زیر را هم به کلاس AddRequest اضافه نمایید.  

private InstanceStore _instanceStore;

این ارجاعیه  به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده می‌کنیم و کد‌های زیر را هم به رویداد Load فرم اضافه می‌کنیم.  

_instanceStore = new SqlWorkflowInstanceStore(_connectionString);
            InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
            _instanceStore.DefaultInstanceOwner = view.InstanceOwner;

InstanceStore یک کلاس abstract  می باشد که همه‌ی Provider‌های مربوط به persistence از آن مشتق می‌شوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflow‌ها را در دیتابیسSQL Server ذخیره کنم.

برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد می‌کنیم و آن را در فرآیند به جریان می‌اندازیم. این کار را در رویداد کلیک دکمه Register انجام می‌دهیم 

private void brnRegister_Click(object sender, RoutedEventArgs e)
        {
            Dictionary<string, object> parameters = new Dictionary<string, object>();
            parameters.Add("Name", txtName.Text);
            parameters.Add("Phone", txtPhone.Text);
            parameters.Add("Email", txtEmail.Text);
            parameters.Add("ConnectionString", _connectionString);
            parameters.Add("Writer", new ListBoxTextWriter(lstEvents));
            WorkflowApplication i = new WorkflowApplication
            (new UserWF(), parameters);
            // Setup persistence 
            i.InstanceStore = _instanceStore;
            i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
            i.Run(); 
        }

پارامتر‌های ورودی را از روی فرم مقدار دهی می‌کنیم. یک شی از کلاس WorkflowApplication ایجاد می‌کنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی می‌کنیم. توسط رویداد PersistableIdle فرآیند رو مجبور می‌کنیم به Persist شدن و Unload شدن.

و سپس فرایند را اجرا می‌کنم.

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

public static void NewUser(User l)
        {
            if (_app != null)
                _app.AddNewUser(l);
        }

همین طور که می‌بینید، یک متد هم در کلاس AddRequest ایجاد می‌شود؛ با این محتوا 

public void AddNewUser(User l)
        {
            this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l)));
        }

این متد فقط یک کاربر را به لیست کاربران اضافه می‌کند. این لیست همه کاربران را نشان می‌دهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم می‌کنیم؛ که نمایش دکمه تایید است. 

private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0)
            {
                User l = (User)lstUsers.Items[lstUsers.SelectedIndex];
                lblSelectedNotes.Visibility = Visibility.Visible;
                if (l.Status == "New")
                {
                    lblAgent.Visibility = Visibility.Visible;
                    txtAcceptedBy.Visibility = Visibility.Visible;
                    btnAccept.Visibility = Visibility.Visible;
                }
                else
                {
                    lblAgent.Visibility = Visibility.Hidden;
                    txtAcceptedBy.Visibility = Visibility.Hidden;
                    btnAccept.Visibility = Visibility.Hidden;
                }
            }
            else
            {
                lblSelectedNotes.Content = "";
                lblSelectedNotes.Visibility = Visibility.Hidden;
                lblAgent.Visibility = Visibility.Hidden;
                txtAcceptedBy.Visibility = Visibility.Hidden;
                btnAccept.Visibility = Visibility.Hidden;
            } 
        }

و برای رویداد کلیک دکمه تایید کاربر : 

private void btnAccept_Click(object sender, RoutedEventArgs e)
        {
            if (lstUsers.SelectedIndex >= 0) 
             { 
                 User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; 
                 Guid id = u.WorkflowID.Value;
                 UserDataDataContext dc = new UserDataDataContext(_connectionString); 
                 dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
                 u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); 
                 if (u != null) 
                 { 
                     u.AcceptedBy = txtAcceptedBy.Text; 
                     u.Status = "Assigned"; 
                     dc.SubmitChanges();
                     // Clear the input 
                     txtAcceptedBy.Text = "";
                 }
                 // Update the grid 
                 lstUsers.Items[lstUsers.SelectedIndex] = u;
                 lstUsers.Items.Refresh();
                 WorkflowApplication i = new WorkflowApplication(new UserWF());
                 i.InstanceStore = _instanceStore;
                 i.PersistableIdle = (waiea) => PersistableIdleAction.Unload;
                 i.Load(id);
                 try
                 {
                     i.ResumeBookmark("GetAcceptes", u);
                 }
                 catch (Exception e2)
                 {
                     AddEvent(e2.Message);
                 }
             } 
        }

کاربر را انتخاب می‌کنم مقادیرش را تنظیم می‌کنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load می‌کنیم و همانطور که می‌بینید توسط متد ResumeBookmark فرآیند رو از جایی که می‌خواهیم ادامه می‌دهیم. البته می‌توان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity  مرتبط با آن تقریبا تکراری است با اجازه‌ی شما من اون رو ننوشتم و زحمتش با خودتونه.

حالا فقط مانده‌است که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:

private void LoadExistingLeads()
        {
            UserDataDataContext dc = new UserDataDataContext(_connectionString);
            dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users);
            IEnumerable<User> q = dc.Users;
            foreach (User u in q)
            {
                AddNewUser(u);
            }
        }

و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار می‌کنیم.

پروژه رو اجرا کرده و یک کاربر را اضافه می‌کنم. همانطور که می‌دانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره می‌شود

برنامه را می‌بندم و دوباره اجرا می‌کنم. کاربر را انتخاب می‌کنم و یک نام برای admin انتخاب و آن را تایید می‌کنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان می‌رسد. با بسته شدن برنامه، فرایند Idle و Unload می‌شود و ذخیره آن در sqlserver صورت می‌گیرد. 

مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت دوم
در قسمت قبل معماری اپلیکیشن‌های N-Tier و بروز رسانی موجودیت‌های منفصل توسط Web API را بررسی کردیم. در این قسمت بروز رسانی موجودیت‌های منفصل توسط WCF را بررسی می‌کنیم.

بروز رسانی موجودیت‌های منفصل توسط WCF

سناریویی را در نظر بگیرید که در آن عملیات CRUD توسط WCF پیاده سازی شده اند و دسترسی داده‌ها با مدل Code-First انجام می‌شود. فرض کنید مدل اپلیکیشن مانند تصویر زیر است.

همانطور که می‌بینید مدل ما متشکل از پست‌ها و نظرات کاربران است. برای ساده نگاه داشتن مثال جاری، اکثر فیلدها حذف شده اند. مثلا متن پست ها، نویسنده، تاریخ و زمان انتشار و غیره. می‌خواهیم تمام کد دسترسی داده‌ها را در یک سرویس WCF پیاده سازی کنیم تا کلاینت‌ها بتوانند عملیات CRUD را توسط آن انجام دهند. برای ساختن این سرویس مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع Class Library بسازید و نام آن را به Recipe2 تغییر دهید.
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • سه کلاس با نام‌های Post, Comment و Recipe2Context به پروژه اضافه کنید. کلاس‌های Post و Comment موجودیت‌های مدل ما هستند که به جداول متناظرشان نگاشت می‌شوند. کلاس Recipe2Context آبجکت DbContext ما خواهد بود که بعنوان درگاه عملیاتی EF عمل می‌کند. دقت کنید که خاصیت‌های لازم WCF یعنی DataContract و DataMember در کلاس‌های موجودیت‌ها بدرستی استفاده می‌شوند. لیست زیر کد این کلاس‌ها را نشان می‌دهد.
[DataContract(IsReference = true)]
public class Post
{
    public Post()
    {
        comments = new HashSet<Comments>();
    }
    
    [DataMember]
    public int PostId { get; set; }
    [DataMember]
    public string Title { get; set; }
    [DataMember]
    public virtual ICollection<Comment> Comments { get; set; }
}

[DataContract(IsReference=true)]
public class Comment
{
    [DataMember]
    public int CommentId { get; set; }
    [DataMember]
    public int PostId { get; set; }
    [DataMember]
    public string CommentText { get; set; }
    [DataMember]
    public virtual Post Post { get; set; }
}

public class EFRecipesEntities : DbContext
{
    public EFRecipesEntities() : base("name=EFRecipesEntities") {}

    public DbSet<Post> posts;
    public DbSet<Comment> comments;
}
  • یک فایل App.config به پروژه اضافه کنید و رشته اتصال زیر را به آن اضافه نمایید.
<connectionStrings>
  <add name="Recipe2ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • حال یک پروژه WCF به Solution جاری اضافه کنید. برای ساده نگاه داشتن مثال جاری، نام پیش فرض Service1 را بپذیرید. فایل IService1.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید.
[ServiceContract]
public interface IService1
{
    [OperationContract]
    void Cleanup();
    [OperationContract]
    Post GetPostByTitle(string title);
    [OperationContract]
    Post SubmitPost(Post post);
    [OperationContract]
    Comment SubmitComment(Comment comment);
    [OperationContract]
    void DeleteComment(Comment comment);
}
  • فایل Service1.svc.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. بیاد داشته باشید که پروژه Recipe2 را ارجاع کنید و فضای نام آن را وارد نمایید. همچنین کتابخانه EF 6 را باید به پروژه اضافه کنید.
public class Service1 : IService
{
    public void Cleanup()
    {
        using (var context = new EFRecipesEntities())
        {
            context.Database.ExecuteSqlCommand("delete from [comments]");
            context. Database.ExecuteSqlCommand ("delete from [posts]");
        }
    }

    public Post GetPostByTitle(string title)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Configuration.ProxyCreationEnabled = false;
            var post = context.Posts.Include(p => p.Comments).Single(p => p.Title == title);
            return post;
        }
    }

    public Post SubmitPost(Post post)
    {
        context.Entry(post).State =
            // if Id equal to 0, must be insert; otherwise, it's an update
            post.PostId == 0 ? EntityState.Added : EntityState.Modified;
        context.SaveChanges();
        return post;
    }

    public Comment SubmitComment(Comment comment)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Comments.Attach(comment);
            if (comment.CommentId == 0)
            {
                // this is an insert
                context.Entry(comment).State = EntityState.Added);
            }
            else
            {
                // set single property to modified, which sets state of entity to modified, but
                // only updates the single property – not the entire entity
                context.entry(comment).Property(x => x.CommentText).IsModified = true;
            }
            context.SaveChanges();
            return comment;
        }
    }

    public void DeleteComment(Comment comment)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Entry(comment).State = EntityState.Deleted;
            context.SaveChanges();
        }
    }
}


  • در آخر پروژه جدیدی از نوع Windows Console Application به Solution جاری اضافه کنید. از این اپلیکیشن بعنوان کلاینتی برای تست سرویس WCF استفاده خواهیم کرد. فایل program.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. روی نام پروژه کلیک راست کرده و گزینه Add Service Reference را انتخاب کنید، سپس ارجاعی به سرویس Service1 اضافه کنید. رفرنسی هم به کتابخانه کلاس‌ها که در ابتدای مراحل ساختید باید اضافه کنید.
class Program
{
    static void Main(string[] args)
    {
        using (var client = new ServiceReference2.Service1Client())
        {
            // cleanup previous data
            client.Cleanup();
            // insert a post
            var post = new Post { Title = "POCO Proxies" };
            post = client.SubmitPost(post);
            // update the post
            post.Title = "Change Tracking Proxies";
            client.SubmitPost(post);
            // add a comment
            var comment1 = new Comment { CommentText = "Virtual Properties are cool!", PostId = post.PostId };
            var comment2 = new Comment { CommentText = "I use ICollection<T> all the time", PostId = post.PostId };
            comment1 = client.SubmitComment(comment1);
            comment2 = client.SubmitComment(comment2);
            // update a comment
            comment1.CommentText = "How do I use ICollection<T>?";
            client.SubmitComment(comment1);
            // delete comment 1
            client.DeleteComment(comment1);
            // get posts with comments
            var p = client.GetPostByTitle("Change Tracking Proxies");
            Console.WriteLine("Comments for post: {0}", p.Title);
            foreach (var comment in p.Comments)
            {
                Console.WriteLine("\tComment: {0}", comment.CommentText);
            }
        }
    }
}
اگر اپلیکیشن کلاینت (برنامه کنسول) را اجرا کنید با خروجی زیر مواجه می‌شوید.

Comments for post: Change Tracking Proxies
Comment: I use ICollection<T> all the time


شرح مثال جاری

ابتدا با اپلیکیشن کنسول شروع می‌کنیم، که کلاینت سرویس ما است. نخست در یک بلاک {} using وهله ای از کلاینت سرویس مان ایجاد می‌کنیم. درست همانطور که وهله ای از یک EF Context می‌سازیم. استفاده از بلوک‌های using توصیه می‌شود چرا که متد Dispose بصورت خودکار فراخوانی خواهد شد، چه بصورت عادی چه هنگام بروز خطا. پس از آنکه وهله ای از کلاینت سرویس را در اختیار داشتیم، متد Cleanup را صدا می‌زنیم. با فراخوانی این متد تمام داده‌های تست پیشین را حذف می‌کنیم. در چند خط بعدی، متد SubmitPost را روی سرویس فراخوانی می‌کنیم. در پیاده سازی فعلی شناسه پست را بررسی می‌کنیم. اگر مقدار شناسه صفر باشد، خاصیت State موجودیت را به Added تغییر می‌دهید تا رکورد جدیدی ثبت کنیم. در غیر اینصورت فرض بر این است که چنین موجودیتی وجود دارد و قصد ویرایش آن را داریم، بنابراین خاصیت State را به Modified تغییر می‌دهیم. از آنجا که مقدار متغیرهای int بصورت پیش فرض صفر است، با این روش می‌توانیم وضعیت پست‌ها را مشخص کنیم. یعنی تعیین کنیم رکورد جدیدی باید ثبت شود یا رکوردی موجود بروز رسانی گردد. رویکردی بهتر آن است که پارامتری اضافی به متد پاس دهیم، یا متدی مجزا برای ثبت رکوردهای جدید تعریف کنیم. مثلا رکوردی با نام InsertPost. در هر حال، بهترین روش بستگی به ساختار اپلیکیشن شما دارد.

اگر پست جدیدی ثبت شود، خاصیت PostId با مقدار مناسب جدید بروز رسانی می‌شود و وهله پست را باز می‌گردانیم. ایجاد و بروز رسانی نظرات کاربران مشابه ایجاد و بروز رسانی پست‌ها است، اما با یک تفاوت اساسی: بعنوان یک قانون، هنگام بروز رسانی نظرات کاربران تنها فیلد متن نظر باید بروز رسانی شود. بنابراین با فیلدهای دیگری مانند تاریخ انتشار و غیره اصلا کاری نخواهیم داشت. بدین منظور تنها خاصیت CommentText را بعنوان Modified علامت گذاری می‌کنیم. این امر منجر می‌شود که Entity Framework عبارتی برای بروز رسانی تولید کند که تنها این فیلد را در بر می‌گیرد. توجه داشته باشید که این روش تنها در صورتی کار می‌کند که بخواهید یک فیلد واحد را بروز رسانی کنید. اگر می‌خواستیم فیلدهای بیشتری را در موجودیت Comment بروز رسانی کنیم، باید مکانیزمی برای ردیابی تغییرات در سمت کلاینت در نظر می‌گرفتیم. در مواقعی که خاصیت‌های متعددی می‌توانند تغییر کنند، معمولا بهتر است کل موجودیت بروز رسانی شود تا اینکه مکانیزمی پیچیده برای ردیابی تغییرات در سمت کلاینت پیاده گردد. بروز رسانی کل موجودیت بهینه‌تر خواهد بود.

برای حذف یک دیدگاه، متد Entry را روی آبجکت DbContext فراخوانی می‌کنیم و موجودیت مورد نظر را بعنوان آرگومان پاس می‌دهیم. این امر سبب می‌شود که موجودیت مورد نظر بعنوان Deleted علامت گذاری شود، که هنگام فراخوانی متد SaveChanges اسکریپت لازم برای حذف رکورد را تولید خواهد کرد.

در آخر متد GetPostByTitle یک پست را بر اساس عنوان پیدا کرده و تمام نظرات کاربران مربوط به آن را هم بارگذاری می‌کند. از آنجا که ما کلاس‌های POCO را پیاده سازی کرده ایم، Entity Framework آبجکتی را بر می‌گرداند که Dynamic Proxy نامیده می‌شود. این آبجکت پست و نظرات مربوط به آن را در بر خواهد گرفت. متاسفانه WCF نمی‌تواند آبجکت‌های پروکسی را مرتب سازی (serialize) کند. اما با غیرفعال کردن قابلیت ایجاد پروکسی‌ها (ProxyCreationEnabled=false) ما به Entity Framework می‌گوییم که خود آبجکت‌های اصلی را بازگرداند. اگر سعی کنید آبجکت پروکسی را سریال کنید با پیغام خطای زیر مواجه خواهید شد:

The underlying connection was closed: The connection was closed unexpectedly 

می توانیم غیرفعال کردن تولید پروکسی را به متد سازنده کلاس سرویس منتقل کنیم تا روی تمام متدهای سرویس اعمال شود.

در این قسمت دیدیم چگونه می‌توانیم از آبجکت‌های POCO برای مدیریت عملیات CRUD توسط WCF استفاده کنیم. از آنجا که هیچ اطلاعاتی درباره وضعیت موجودیت‌ها روی کلاینت ذخیره نمی‌شود، متدهایی مجزا برای عملیات CRUD ساختیم. در قسمت‌های بعدی خواهیم دید چگونه می‌توان تعداد متدهایی که سرویس مان باید پیاده سازی کند را کاهش داد و چگونه ارتباطات بین کلاینت و سرور را ساده‌تر کنیم.

مطالب
ایجاد سرویس چندلایه‎ی WCF با Entity Framework در قالب پروژه - 7
خروجی پروژه‌ی WCF Service Library یک فایل DLL است که هنگامی که با کنسول WCF Test Client اجرا می‌شود در آدرسی که در Web.Config تنظیم کرده بودیم اجرا می‌شود. اگر یک پروژه‌ی ویندوزی در همین راه حل بسازیم؛ خواهیم توانست از این آدرس برای دسترسی به WCF بهره ببریم. ولی اگر بخواهیم در IIS سرور قرار دهیم؛ باید در وب‌سایت آن‌را میزبانی کنیم. برای این‌کار از Solution Explorer روی راه حل MyNews راست‌کلیک کنید و از منوی باز شده روی Add -> New Web Site کلیک کنید. سپس مراحل زیر را برابر با شکل‌های زیر انجام دهید:

سپس روی Web Site ایجادشده راست کلیک کنید و از منوی بازشده Property Pages را انتخاب کنید. روی گزینه‌ی Add Reference کلیک کنید، سپس پروژه‌ی MyNewsWCFLibrary را از قسمت Solution انتخاب کرده و دکمه‌ی OK را بفشارید. 

دکمه‌ی OK را بفشارید و از Solution Explorer فایل Web.Config را باز کنید. پیش از تغییرات مد نظر باید چنین محتوایی داشته باشد:

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
</configuration>

متن آن‌را به این صورت تغییر دهید:

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=169433
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
  <system.serviceModel>
    <serviceHostingEnvironment>
      <serviceActivations>
        <add factory="System.ServiceModel.Activation.ServiceHostFactory" relativeAddress="./HamedService.svc" service="MyNewsWCFLibrary.MyNewsService"/>
      </serviceActivations>
    </serviceHostingEnvironment>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

همان‌‌گونه که مشاهده می‌کنید به وسیله‌ی تگ add factory سرویس‌ها را به وب‌سایت معرفی می‌کنیم. با relativeAddress می‌توانیم هر نامی را به عنوان نام سرویس که در URL قرار می‌گیرد معرفی کنیم. چنان‌که من یه جای  MyNewsService از نام HamedService استفاده کردم. و در صفت  service فضای نام و نام کلاس سرویس را معرفی می‌کنیم. 

اکنون پروژه را اجرا کنید. در مرورگر باید صفحه را به این‌صورت مشاهده کنید:

نیازی به یادآوری نیست که شما می‌توانید این پروژه را در IIS سرور راه‌اندازی کنید تا کلیه‌ی مشتری‌ها به آن دسترسی داشته باشند. هرچند پیش از آن باید امنیت را نیز در WCF برقرار کنید.

توجه داشته باشید که روشی که در این بخش به عنوان میزبانی WCF مطرح کردم یکی از روش‌های میزبانی WCF است. مثلاً شما می‌توانستید به جای ایجاد یک WCFLibrary و یک Web Site به صورت جداگانه یک پروژه از نوع WCF Service و یا Web Site ایجاد می‌کردید و سرویس‌ها و مدل Entity Framework را به طور مستقیم در آن می‌افزودید. روشی که در این درس از آن بهره برده ایم البته مزایایی دارد از جمله این‌که خروجی پروژه فقط یک فایل DLL است و با هر بار تغییر فقط کافی است همان فایل را در پوشه Bin از وب‌سایتی که روی سرور می‌گذارید کپی کنید. 

در بخش هشتم با هم یک پروژه‌ی تحت ویندوز خواهیم ساخت و از سرویس WCF ای که ساخته ایم در آن استفاده خواهیم کرد.

نظرات مطالب
معرفی System.Text.Json در NET Core 3.0.
یک نکته‌ی تکمیلی: نوشتن تبدیلگرهای نوع‌ها برای System.Text.Json

System.Text.Json، در حال حاضر از مفهومی به نام type coercion/inference، پشتیبانی نمی‌کند. type coercion یعنی تبدیل یک مقدار، به مقداری دیگر که به صورت مستقیم قابل انتساب به یکدیگر نیستند. برای مثال اگر رشته‌ی "true" را درنظر بگیریم، قابلیت انتساب به یک خاصیت از نوع bool را ندارد. برای یک چنین مواردی در این API جدید، باید تبدیلگر نوشت.
یک مثال:
using System.Collections.Generic;
using System.Text.Json;

namespace JsonTests
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsInStock { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {            
         var products = JsonSerializer.Deserialize<List<Product>>("[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]");
        }
    }
}
در این مثال، خاصیت IsInStock از نوع bool است، اما مقداری را که باید از طریق متد Deserialize دریافت کنیم، یک رشته‌ی bool ای است که قابل انتساب به bool نیست. در این حالت اگر برنامه را اجرا کنیم به استثنای زیر خواهیم رسید:
An unhandled exception of type 'System.Text.Json.JsonException' occurred in System.Text.Json.dll
Inner exceptions found, see $exception in variables window for more details.
Innermost exception System.InvalidOperationException : Cannot get the value of a token type 'String' as a boolean.
برای رفع این مشکل، می‌توان تبدیلگر زیر را تدارک دید:
    public class BooleanConverter : JsonConverter<bool>
    {
        public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var value = reader.GetString();
            if (value.Equals("true", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("yes", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("1", StringComparison.Ordinal))
            {
                return true;
            }

            if (value.Equals("false", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("no", StringComparison.OrdinalIgnoreCase)
                 || value.Equals("0", StringComparison.Ordinal))
            {
                return false;
            }

            throw new NotSupportedException($"`{value}` can't be converted to `bool`.");
        }

        public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
        {
            switch (value)
            {
                case true:
                    writer.WriteStringValue("true");
                    break;
                case false:
                    writer.WriteStringValue("false");
                    break;
            }
        }
    }
برای نوشتن یک تبدیلگر bool، کلاس مرتبط، باید <JsonConverter<bool را پیاده سازی کند. کلاس JsonConverter نیز به صورت زیر تعریف شده‌است:
    public abstract class JsonConverter<T> : JsonConverter
    {
        protected internal JsonConverter();

        public override bool CanConvert(Type typeToConvert);
        public abstract T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options);
        public abstract void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options);
    }
و پیاده سازی دو متد Read و Write آن الزامی است.
در متد Read آن، مقدار رشته‌ای دریافت شده‌ی از منبع داده، در اختیار ما قرار می‌گیرد. سپس باید بر اساس این مقدار، مقدار متناظری را از نوع T که در اینجا bool است، بازگشت دهیم. برای مثال اگر یکی از مقادیر رشته‌ای true ،yes و 1 را دریافت کردیم، بجای آن true را بازگشت می‌دهیم.

اکنون برای استفاده‌ی از آن خواهیم داشت:
var options = new JsonSerializerOptions();
options.Converters.Add(new BooleanConverter());
var products = JsonSerializer.Deserialize<List<Product>>(
   "[{\"Id\":1026,\"Name\":\"P1\",\"IsInStock\":\"false\"}]",
   options);
و یا روش دیگر انجام اینکار، استفاده از ویژگی JsonConverter، برای معرفی تبدیلگر تهیه شده‌است:
[JsonConverter(typeof(BooleanConverter))]
public bool IsInStock { get; set; }

از این تبدیلگر برای حالت Serialize نیز می‌توان استفاده کرد:
var options  = new JsonSerializerOptions() {WriteIndented = true };
options.Converters.Add(new BooleanConverter());
var data = JsonSerializer.Serialize<List<Product>>(productList, options);
نظرات مطالب
محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC
لازم نیست مدیریت کنید. هدف این بوده که نظم جاری را تغییر دهد. این استثناء توسط ELmah دریافت و ثبت خواهد شد. همچنین کاربر به یکی از صفحات پیش فرض خطای برنامه هدایت می‌شود و متوجه خواهد شد که خطایی رخ داده است.
ولی در کل می‌شود IExceptionFilter را نیز پیاده سازی و مدیریت کرد:
public class CustomFilter : FilterAttribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {

مطالب
تولید هدرهای Content Security Policy توسط ASP.NET Core برای برنامه‌های Angular
پیشتر مطلب «افزودن هدرهای Content Security Policy به برنامه‌های ASP.NET» را در این سایت مطالعه کرده‌اید. در اینجا قصد داریم معادل آن‌را برای ASP.NET Core تهیه کرده و همچنین نکات مرتبط با برنامه‌های Angular را نیز در آن لحاظ کنیم.


تهیه میان افزار افزودن هدرهای Content Security Policy

کدهای کامل این میان افزار را در ادامه مشاهده می‌کنید:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;

namespace AngularTemplateDrivenFormsLab.Utils
{
    public class ContentSecurityPolicyMiddleware
    {
        private readonly RequestDelegate _next;

        public ContentSecurityPolicyMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public Task Invoke(HttpContext context)
        {
            context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
            context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
            context.Response.Headers.Add("X-Content-Type-Options", "nosniff");

            string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
            context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
            return _next(context);
        }
    }

    public static class ContentSecurityPolicyMiddlewareExtensions
    {
        /// <summary>
        /// Make sure you add this code BEFORE app.UseStaticFiles();,
        /// otherwise the headers will not be applied to your static files.
        /// </summary>
        public static IApplicationBuilder UseContentSecurityPolicy(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<ContentSecurityPolicyMiddleware>();
        }
    }
}
که نحوه‌ی استفاده از آن در کلاس آغازین برنامه به صورت ذیل خواهد بود:
public void Configure(IApplicationBuilder app)
{
   app.UseContentSecurityPolicy();

توضیحات تکمیلی

افزودن X-Frame-Options
 context.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN");
از هدر X-FRAME-OPTIONS، جهت منع نمایش و رندر سایت جاری، در iframeهای سایت‌های دیگر استفاده می‌شود. ذکر مقدار SAMEORIGIN آن، به معنای مجاز تلقی کردن دومین جاری برنامه است.


افزودن X-Xss-Protection
 context.Response.Headers.Add("X-Xss-Protection", "1; mode=block");
تقریبا تمام مرورگرهای امروزی قابلیت تشخیص حملات XSS را توسط static analysis توکار خود دارند. این هدر، آنالیز اجباری XSS را فعال کرده و همچنین تنظیم حالت آن به block، نمایش و رندر قسمت مشکل‌دار را به طور کامل غیرفعال می‌کند.


افزودن X-Content-Type-Options
 context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
وجود این هدر سبب می‌شود تا مرورگر، حدس‌زدن نوع فایل‌ها، درخواست‌ها و محتوا را کنار گذاشته و صرفا به content-type ارسالی توسط سرور اکتفا کند. به این ترتیب برای مثال امکان لینک کردن یک فایل غیرجاوا اسکریپتی و اجرای آن به صورت کدهای جاوا اسکریپت، چون توسط تگ script ذکر شده‌است، غیرفعال می‌شود. در غیراینصورت مرورگر هرچیزی را که توسط تگ script به صفحه لینک شده باشد، صرف نظر از content-type واقعی آن، اجرا خواهد کرد.


افزودن Content-Security-Policy
string[] csp =
            {
              "default-src 'self'",
              "style-src 'self' 'unsafe-inline'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
              "font-src 'self'",
              "img-src 'self' data:",
              "connect-src 'self'",
              "media-src 'self'",
              "object-src 'self'",
              "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
            };
context.Response.Headers.Add("Content-Security-Policy", string.Join("; ", csp));
وجود این هدر، تزریق کدها و منابع را از دومین‌های دیگر غیرممکن می‌کند. برای مثال ذکر self در اینجا به معنای مجاز بودن الصاق و اجرای اسکریپت‌ها، شیوه‌نامه‌ها، تصاویر و اشیاء، صرفا از طریق دومین جاری برنامه است و هرگونه منبعی که از دومین‌های دیگر به برنامه تزریق شود، قابلیت اجرایی و یا نمایشی نخواهد داشت.

در اینجا ذکر unsafe-inline و unsafe-eval را مشاهده می‌کنید. برنامه‌های Angular به همراه شیوه‌نامه‌های inline و یا بکارگیری متد eval در مواردی خاص هستند. اگر این دو گزینه ذکر و فعال نشوند، در کنسول developer مرورگر، خطای بلاک شدن آن‌ها را مشاهده کرده و همچنین برنامه از کار خواهد افتاد.

یک نکته: با فعالسازی گزینه‌ی aot-- در حین ساخت برنامه، می‌توان unsafe-eval را نیز حذف کرد.


استفاده از فایل web.config برای تعریف SameSite Cookies

یکی از پیشنهادهای اخیر ارائه شده‌ی جهت مقابله‌ی با حملات CSRF و XSRF، قابلیتی است به نام  Same-Site Cookies. به این ترتیب مرورگر، کوکی سایت جاری را به همراه یک درخواست ارسال آن به سایت دیگر، پیوست نمی‌کند (کاری که هم اکنون با درخواست‌های Cross-Site صورت می‌گیرد). برای رفع این مشکل، با این پیشنهاد امنیتی جدید، تنها کافی است SameSite، به انتهای کوکی اضافه شود:
 Set-Cookie: sess=abc123; path=/; SameSite

نگارش‌های بعدی ASP.NET Core، ویژگی SameSite را نیز به عنوان CookieOptions لحاظ کرده‌اند. همچنین یک سری از کوکی‌های خودکار تولیدی توسط آن مانند کوکی‌های anti-forgery به صورت خودکار با این ویژگی تولید می‌شوند.
اما مدیریت این مورد برای اعمال سراسری آن، با کدنویسی میسر نیست (مگر اینکه مانند نگارش‌های بعدی ASP.NET Core پشتیبانی توکاری از آن صورت گیرد). به همین جهت می‌توان از ماژول URL rewrite مربوط به IIS برای افزودن ویژگی SameSite به تمام کوکی‌های تولید شده‌ی توسط سایت، کمک گرفت. برای این منظور تنها کافی است فایل web.config را ویرایش کرده و موارد ذیل را به آن اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <clear />
        <!-- https://scotthelme.co.uk/csrf-is-dead/ -->
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=lax" />
          <conditions></conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>


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

اگر به هدر Content-Security-Policy دقت کنید، گزینه‌ی آخر آن، ذکر اکشن متدی در سمت سرور است:
   "report-uri /api/CspReport/Log" //TODO: Add api/CspReport/Log
با تنظیم این مورد، می‌توان موارد بلاک شده را در سمت سرور لاگ کرد. اما این اطلاعات ارسالی به سمت سرور، فرمت خاصی را دارند:
{
  "csp-report": {
    "document-uri": "http://localhost:5000/untypedSha",
    "referrer": "",
    "violated-directive": "script-src",
    "effective-directive": "script-src",
    "original-policy": "default-src 'self'; style-src 'self'; script-src 'self'; font-src 'self'; img-src 'self' data:; connect-src 'self'; media-src 'self'; object-src 'self'; report-uri /api/Home/CspReport",
    "disposition": "enforce",
    "blocked-uri": "eval",
    "line-number": 21,
    "column-number": 8,
    "source-file": "http://localhost:5000/scripts.bundle.js",
    "status-code": 200,
    "script-sample": ""
  }
}
به همین جهت ابتدا نیاز است توسط JsonProperty کتابخانه‌ی JSON.NET، معادل این خواص را تولید کرد:
    class CspPost
    {
        [JsonProperty("csp-report")]
        public CspReport CspReport { get; set; }
    }

    class CspReport
    {
        [JsonProperty("document-uri")]
        public string DocumentUri { get; set; }

        [JsonProperty("referrer")]
        public string Referrer { get; set; }

        [JsonProperty("violated-directive")]
        public string ViolatedDirective { get; set; }

        [JsonProperty("effective-directive")]
        public string EffectiveDirective { get; set; }

        [JsonProperty("original-policy")]
        public string OriginalPolicy { get; set; }

        [JsonProperty("disposition")]
        public string Disposition { get; set; }

        [JsonProperty("blocked-uri")]
        public string BlockedUri { get; set; }

        [JsonProperty("line-number")]
        public int LineNumber { get; set; }

        [JsonProperty("column-number")]
        public int ColumnNumber { get; set; }

        [JsonProperty("source-file")]
        public string SourceFile { get; set; }

        [JsonProperty("status-code")]
        public string StatusCode { get; set; }

        [JsonProperty("script-sample")]
        public string ScriptSample { get; set; }
    }
اکنون می‌توان بدنه‌ی درخواست را استخراج و سپس به این شیء ویژه نگاشت کرد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class CspReportController : Controller
    {
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> Log()
        {
            CspPost cspPost;
            using (var bodyReader = new StreamReader(this.HttpContext.Request.Body))
            {
                var body = await bodyReader.ReadToEndAsync().ConfigureAwait(false);
                this.HttpContext.Request.Body = new MemoryStream(Encoding.UTF8.GetBytes(body));
                cspPost = JsonConvert.DeserializeObject<CspPost>(body);
            }

            //TODO: log cspPost

            return Ok();
        }
    }
}
در اینجا نحوه‌ی استخراج Request.Body را به صورت خام را مشاهده می‌کنید. سپس توسط متد DeserializeObject کتابخانه‌ی JSON.NET، این رشته به شیء CspPost نگاشت شده‌است.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
ASP.NET MVC #21

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

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


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

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

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

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

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

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


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


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

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

using System;

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

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


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

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

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

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

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

using System.Collections.Generic;

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

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

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

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

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

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

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

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

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

@model MvcApplication18.Models.Employee

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

@RenderSection("javascript", required: false)



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

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

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

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

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

return Json(data);
}

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

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

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

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


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

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

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

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

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

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

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

@model IEnumerable<MvcApplication18.Models.Employee>

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

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

</table>

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

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

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

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


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


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

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

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

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

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

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

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

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

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


نظرات مطالب
آموزش سیلورلایت 4 - قسمت‌های 21 تا 27
WCF RIA Services این امکان رو به شما می‌ده که به منطق پیاده سازی شده در سمت سرور در برنامه‌ی سمت کلاینت خودتون (یعنی همان برنامه سیلورلایت) به شکل شکیلی دسترسی داشته باشید. به این صورت دیگر نیازی نخواهد بود مدلی را سمت کاربر پیاده سازی کنید، اعتبار سنجی را این طرف هم مجددا اعمال کنید، هر بار که WCF Service تغییر کرد، در سمت کلاینت مجبور به به روز رسانی باشید و خیلی موارد دیگر. تمام این‌ها یکبار در سمت سرور پیاده سازی می‌شود و سپس توسط فریم ورک WCF RIA Services در سمت کلاینت قابل دسترسی خواهد بود.
بنابراین مدل قابل استفاده در سمت کلاینت هم از این سرویس سمت سرور دریافت می‌شود و جالب اینجا است که تمام مباحث مطلع سازی تغییرات خواص، اعتبار سنجی، نکات ریز binding و غیره در این مدل‌های WCF RIA Services به صورت خودکار گنجانده شده و بار کدنویسی شما بسیار کمتر می‌شود.
برای MVVM ، کنترل domain data source را با کد نویسی تولید می‌کنم تا بتونم در View Model استفاده کنم و از حالت متداول کشیدن و رها کردن این کنترل روی فرم که با اصول MVVM سازگار نیست به این صورت رها خواهم شد.
نظرات مطالب
EF Code First #12
ORMها کلا در سیلورلایت مستقیما قابل استفاده نیستند چون سیلورلایت سمت کاربر اجرا می‌شود و دسترسی کاملی هم به کل دات نت ندارد. سیلورلایت از طریق سرویس‌های WCF می‌تونه با سرور ارتباط برقرار کنه و این مباحث در سرویس‌های WCF هم قابل استفاده است.
البته برای سیلورلایت WCF RIA Services تعریف شده که روش مرجح است و در آن امکان دسترسی به EF Code first وجود دارد.