مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
در این مطلب یکی از اهداف Defensive Programming تحت عنوان Predictability مرتبط با متدها را بررسی کرده و تمرکز اصلی، بر روی مقدار بازگشتی متدها خواهد بود. 
پیش نیازها
به طور کلی، نتیجه حاصل از اجرای یک متد می‌تواند یکی از حالت‌های زیر باشد:

متدی تحت عنوان ValidateEmail را تصور کنید. این متد از حیث بازگشت نتیجه به عنوان خروجی می‌تواند به اشکال مختلفی پیاده سازی شود که در ادامه مشاهده می‌کنیم:


متد ValidateEmail با خروجی Boolean

        public bool ValidateEmail(string email)
        {
            var valid = true;
            if (string.IsNullOrWhiteSpace(email))
            {
                valid = false;
            }

            var isValidFormat = true;//todo: using RegularExpression
            if (!isValidFormat)
            {
                valid = false;
            }

            var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
            if (!isRealDoamin)
            {
                valid = false;
            }

            return valid;
        }

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

var email = "email@example.com";
var isValid = ValidateEmail(email);
if(isValid)
{
    //do something
}


متد ValidateEmail با صدور استثناء

        public void ValidateEmail(string email)
        {
            if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullException(nameof(email));

            var isValidFormat = true;//todo: using RegularExpression
            if (!isValidFormat) throw new ArgumentException("email is not in a correct format");

            var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
            if (!isRealDoamin) throw new ArgumentException("email does not include a valid domain.")
        }

روش بالا هم جواب می‌دهد ولی بهتر است کلاس Exception سفارشی به عنوان مثال ValidationException برای این قضیه در نظر گرفته شود تا بتوان وهله‌های صادر شده از این نوع را در لایه‌های بالاتر مدیریت کرد.


متد ValidateEmail با چندین خروجی


برای این منظور چندین راه حل پیش رو داریم.


با استفاده از پارامتر out:

        public bool ValidateEmail(string email, out string message)
        {
            var valid = true;
            message = string.Empty;

            if (string.IsNullOrWhiteSpace(email))
            {
                valid = false;
                message = "email is null.";
            }

            if (valid)
            {
                var isValidFormat = true;//todo: using RegularExpression
                if (!isValidFormat)
                {
                    valid = false;
                    message = "email is not in a correct format";
                }
            }

            if (valid)
            {
                var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
                if (!isRealDoamin)
                {
                    valid = false;
                    message = "email does not include a valid domain.";
                }
            }

            return valid;
        }
و نحوه استفاده از آن:
var email = "email@example.com";
var isValid = ValidateEmail(email, out string message);
if (isValid)
{
    //do something
}
خب کمی بهتر شد؛ ولی امکان دریافت لیست خطاهای اعتبارسنجی را به صورت یکجا نداریم و یک تک پیغام را در اختیار ما قرار می‌دهد. برای بهبود آن می‌توان از یک Tuple به شکل زیر برای تولید خروجی متد بالا نیز استفاده کرد.
Tuple<bool, List<string>> result = Tuple.Create<bool, List<string>>(true, new List<string>());
یا بهتر است یک کلاس مشخصی برای این منظور در نظر گرفت؛ به عنوان مثال:
        public class OperationResult
        {
            public bool Success { get; set; }
            public IList<string> Messages { get; } = new List<string>();

            public void AddMessage(string message)
            {
                Messages.Add(message);
            }
        }
در این صورت بدنه متد ValidateEmail به شکل زیر تغییر خواهد کرد:
        public OperationResult ValidateEmail(string email)
        {
            var result = new OperationResult();

            if (string.IsNullOrWhiteSpace(email))
            {
                result.Success = false;
                result.AddMessage("email is null.");
            }

            if (result.Success)
            {
                var isValidFormat = true;//todo: using RegularExpression
                if (!isValidFormat)
                {
                    result.Success = false;
                    result.AddMessage("email is not in a correct format");
                }
            }

            if (result.Success)
            {
                var isRealDoamin = true;//todo: Code here that confirms whether domain exists.
                if (!isRealDoamin)
                {
                    result.Success = false;
                    result.AddMessage("email does not include a valid domain.");
                }
            }

            return result;
        }

این بار خروجی متد مذکور از نوع OperationResult ای می‌باشد که هم موفقیت آمیز بودن یا عدم آن را مشخص می‌کند و همچنین امکان دسترسی به لیست پیغام‌های مرتبط با اعتبارسنجی‌های انجام شده، وجود دارد.


استفاده از Exception برای نمایش پیغام برای کاربر نهایی

با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، می‌توان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشده‌اند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز می‌باشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع به عنوان یک Cross Cutting Concern تحت عنوان Exception Management آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت.

به عنوان مثال داشتن یک کلاس Exception سفارشی تحت عنوان UserFriendlyException در این راستا یک الزام می‌باشد.

   [Serializable]
   public class UserFriendlyException : Exception
   {
       public string Details { get; private set; }
       public int Code { get; set; }

       public UserFriendlyException()
       {
       }

       public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context)
           : base(serializationInfo, context)
       {

       }

       public UserFriendlyException(string message)
           : base(message)
       {
       }

       public UserFriendlyException(int code, string message)
           : this(message)
       {
           Code = code;
       }

       public UserFriendlyException(string message, string details)
           : this(message)
       {
           Details = details;
       }

       public UserFriendlyException(int code, string message, string details)
           : this(message, details)
       {
           Code = code;
       }

       public UserFriendlyException(string message, Exception innerException)
           : base(message, innerException)
       {
       }

       public UserFriendlyException(string message, string details, Exception innerException)
           : this(message, innerException)
       {
           Details = details;
       }
   }

و همچنین لازم است در بالاترین لایه سیستم خود به عنوان مثال برای یک پروژه ASP.NET MVC یا ASP.NET Core MVC می‌توان یک ExceptionFilter سفارشی نیز تهیه کرد که هم به صورت سراسری استثنا‌ءهای سفارشی شما را مدیریت کند و همچنین خروجی مناسب Json برای استفاده در سمت کلاینت را نیز مهیا کند. به عنوان مثال برای درخواست‌های Ajax ای لازم است در سمت کلاینت نیز پاسخ‌های رسیده از سمت سرور به صورت سراسری مدیریت شوند و برای سایر درخواست‌ها همان نمایش صفحات خطای پیغام مرتبط با استثناء رخ داده شده کفایت می‌کند.


یک مدل پیشنهادی برای تهیه خروجی مناسب برای ارسال جزئیات استثنا رخ داده در درخواست‌های Ajax ای

    [Serializable]
    public class MvcAjaxResponse : MvcAjaxResponse<object>
    {
        public MvcAjaxResponse()
        {
        }

        public MvcAjaxResponse(bool success)
            : base(success)
        {
        }

        public MvcAjaxResponse(object result)
            : base(result)
        {
        }

        public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false)
            : base(error, unAuthorizedRequest)
        {
        }
    }
   

    [Serializable]
    public class MvcAjaxResponse<TResult> : MvcAjaxResponseBase
    {
        public MvcAjaxResponse(TResult result)
        {
            Result = result;
            Success = true;
        }

        public MvcAjaxResponse()
        {
            Success = true;
        }

        public MvcAjaxResponse(bool success)
        {
            Success = success;
        }

        public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false)
        {
            Error = error;
            UnAuthorizedRequest = unAuthorizedRequest;
            Success = false;
        }

        /// <summary>
        ///     The actual result object of AJAX request.
        ///     It is set if <see cref="MvcAjaxResponseBase.Success" /> is true.
        /// </summary>
        public TResult Result { get; set; }
    }

    public class MvcAjaxResponseBase
    {
        public string TargetUrl { get; set; }

        public bool Success { get; set; }

        public ErrorInfo Error { get; set; }

        public bool UnAuthorizedRequest { get; set; }

        public bool __mvc { get; } = true;
    }

و کلاس  ErrorInfo:
    [Serializable]
    public class ErrorInfo
    {
        public int Code { get; set; }
        public string Message { get; set; }
        public string Detail { get; set; }
        public Dictionary<string, string> ValidationErrors { get; set; }

        public ErrorInfo()
        {
        }
        public ErrorInfo(string message)
        {
            Message = message;
        }
        public ErrorInfo(int code)
        {
            Code = code;
        }

        public ErrorInfo(int code, string message)
            : this(message)
        {
            Code = code;
        }

        public ErrorInfo(string message, string details)
            : this(message)
        {
            Detail = details;
        }

        public ErrorInfo(int code, string message, string details)
            : this(message, details)
        {
            Code = code;
        }
    }

یک مثال واقعی
        public async Task CheckIsDeactiveAsync(long id)
        {
            if (await _organizationalUnits.AnyAsync(a => a.Id == id && !a.IsActive).ConfigureAwait(false))
                throw new UserFriendlyException("واحد سازمانی جاری غیرفعال می‌باشد.");
        }

روش نام گذاری متدهایی که امکان بازگشت خروجی Null را دارند

متد زیر را در نظر بگیرید:
public User GetById(long id);
وظیفه این متد یافت و بازگشت یک وهله از کلاس User می‌باشد و نباید خروجی Null تولید کند. در صورتیکه در پیاده سازی آن امکان یافت چنین کاربری نبود، بهتر است یک استثنای سفارشی دیگر شبیه به EntityNotFoundException زیر را صادر کنید:
    [Serializable]
    public class EntityNotFoundException : Exception
    {
        public Type EntityType { get; set; }
        public object Id { get; set; }
        public EntityNotFoundException()
        {
        }

        public EntityNotFoundException(string message)
            : base(message)
        {

        }

        public EntityNotFoundException(string message, Exception innerException)
            : base(message, innerException)
        {
        }

        public EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context)
            : base(serializationInfo, context)
        {

        }
        public EntityNotFoundException(Type entityType, object id)
            : this(entityType, id, null)
        {

        }
        public EntityNotFoundException(Type entityType, object id, Exception innerException)
            : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException)
        {
            EntityType = entityType;
            Id = id;
        }

    }
یا اگر امکان بازگشت مقدار Null را داشته باشد، بهتر است نام آن به GetByIdOrNull تغییر یابد. در این صورت تکلیف استفاده کننده از این متد مشخص می‌باشد.

یک مثال واقعی 

        public async Task<UserOrganizationalUnitInfo> GetCurrentOrganizationalUnitInfoOrNullAsync(long userId)
        {
            return (await _setting.GetSettingValueForUserAsync(
                    UserSettingNames.CurrentOrganizationalUnitInfo, userId).ConfigureAwait(false))
                .FromJsonString<UserOrganizationalUnitInfo>();
        }

نظرات مطالب
Blazor 5x - قسمت 14 - کار با فرم‌ها - بخش 2 - تعریف فرم‌ها و اعتبارسنجی آن‌ها
یک نکته‌ی تکمیلی: امکان اعتبارسنجی دستی فرم‌ها در Blazor

در این مطلب با روش معرفی EditForm و خاصیت Model آن آشنا شدیم که کار اعتبارسنجی را به صورت خودکار مدیریت می‌کند. اگر خواستیم کنترل بیشتری را بر روی این فرآیند داشته باشیم، می‌توان عملیات اعتبارسنجی را دستی کرد:
@implements IDisposable

<EditForm EditContext="@_editContext" OnValidSubmit="submit">


    <button type="submit" disabled="@_isInvalidForm">Submit</button>
</EditForm>

@code
{
    private User _userModel = new User();
    private EditContext _editContext;
    private bool _isInvalidForm = true;


    protected override void OnInitialized()
    {
        _editContext = new EditContext(_userModel);
        _editContext.OnFieldChanged += HandleFieldChanged;
    }

    private void submit()
    {
        if(_editContext.Validate())
        {
           
        }
    }

    private void HandleFieldChanged(object sender, FieldChangedEventArgs e)
    {
        _isInvalidForm = !_editContext.Validate();
        StateHasChanged();
    }

    public void Dispose()
    {
        _editContext.OnFieldChanged -= HandleFieldChanged;
    }
}
اینبار در اینجا بجای استفاده از خاصیت Model، از خاصیت جدید EditContext استفاده می‌شود (تنها یکی از این دو را می‌توان ذکر کرد). روش مقدار دهی EditContext را در روال آغازین کامپوننت مشاهده می‌کنید که وهله‌ای از مدل را دریافت کرده و تحت بررسی قرار می‌دهد. EditContext یک پارامتر آبشاری است و به صورت خودکار در اختیار تمام کنترل‌ها و کامپوننت‌های محصور شده‌ی توسط EditForm قرار می‌گیرد.
نمونه‌ای از روش کار با آن‌را در متد submit مشاهده می‌کنید که باید به همراه فراخوانی متد Validate آن باشد و یا می‌توان به صورت زیر در مورد یک فیلد عمل کرد:
var isValid = !_editContext.GetValidationMessages(fieldIdentifier).Any();
و یا حتی می‌توان با استفاده از رخ‌داد OnFieldChanged، برای مثال بررسی کرد که آیا کل فرم معتبر هست یا خیر؟ و اگر خیر، برای مثال دکمه‌ی submit را غیرفعال کرد. در این حالت همواره بهتر است که پاکسازی رویداد OnFieldChanged را در پایان کار انجام داد، تا برنامه دچار نشتی حافظه نشود.
مطالب
نمایش خطاهای اعتبارسنجی سمت سرور ASP.NET Core در برنامه‌های Angular
در مطلب «فرم‌های مبتنی بر قالب‌ها در Angular - قسمت چهارم - اعتبارسنجی ورودی‌ها» با نحوه‌ی تنظیمات اعتبارسنجی سمت کلاینت برنامه‌های Angular آشنا شدیم. اما اگر مدل سمت سرور ما یک چنین شکلی را داشته باشد که به همراه خطاهای اعتبارسنجی سفارشی نیز هست:
using System;
using System.ComponentModel.DataAnnotations;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class Movie
    {
        public int Id { get; set; }

        [Required(ErrorMessage = "Movie Title is Required")]
        [MinLength(3, ErrorMessage = "Movie Title must be at least 3 characters")]
        public string Title { get; set; }

        [Required(ErrorMessage = "Movie Director is Required.")]
        public string Director { get; set; }

        [Range(0, 100, ErrorMessage = "Ticket price must be between 0 and 100.")]
        public decimal TicketPrice { get; set; }

        [Required(ErrorMessage = "Movie Release Date is required")]
        public DateTime ReleaseDate { get; set; }
    }
}
و همچنین کنترلر و اکشن متد دریافت کننده‌ی آن نیز به صورت ذیل تعریف شده باشد:
using AngularTemplateDrivenFormsLab.Models;
using Microsoft.AspNetCore.Mvc;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class MoviesController : Controller
    {
        [HttpPost]
        public IActionResult Post([FromBody]Movie movie)
        {
            if (ModelState.IsValid)
            {
                // TODO: save ...
                return Ok(movie);
            }

            ModelState.AddModelError("", "This record already exists."); // a cross field validation
            return BadRequest(ModelState);
        }
    }
}
دو نوع خطای اعتبارسنجی سمت سرور را به سمت کلاینت ارسال خواهیم کرد:
الف) خطاهای اعتبارسنجی در سطح فیلدها
زمانیکه return BadRequest(ModelState) صورت می‌گیرد، محتویات شیء ModelState به همراه status code مساوی 400 به سمت کلاینت ارسال خواهد شد. در شیء ModelState یک دیکشنری که کلیدهای آن، نام خواص و مقادیر متناظر با آن‌ها، خطاهای اعتبارسنجی تنظیم شده‌ی در مدل است، قرار دارند.
ب) خطاهای اعتبارسنجی عمومی
در این بین می‌توان دیکشنری ModelState را توسط متد AddModelError نیز تغییر داد و برای مثال کلید آن‌را مساوی "" تعریف کرد. در این حالت یک چنین خطایی به کل فرم اشاره می‌کند و نه به یک خاصیت خاص.

نمونه‌ای از خروجی نهایی ارسالی به سمت کاربر:
 {"":["This record already exists."],"TicketPrice":["Ticket price must be between 0 and 100."]}

به همین جهت نیاز است بتوان خطاهای حالت (الف) را دقیقا در ذیل هر فیلد و خطاهای حالت (ب) را در بالای فرم به صورت عمومی به کاربر نمایش داد:



پردازش و دریافت خطاهای اعتبارسنجی سمت سرور در یک برنامه‌ی Angular

با توجه به اینکه سرور، شیء ModelState را توسط return BadRequest به سمت کلاینت ارسال می‌کند، برای پردازش دیکشنری دریافتی از سمت آن، تنها کافی است قسمت بروز خطای عملیات ارسال اطلاعات را بررسی کنیم:


در این HttpErrorResponse دریافتی، دو خاصیت error که همان آرایه‌ی دیکشنری نام خواص و پیام‌های خطای مرتبط با هر کدام و status code دریافتی مهم هستند:
  errors: string[] = [];

  processModelStateErrors(form: NgForm, responseError: HttpErrorResponse) {
    if (responseError.status === 400) {
      const modelStateErrors = responseError.error;
      for (const fieldName in modelStateErrors) {
        if (modelStateErrors.hasOwnProperty(fieldName)) {
          const modelStateError = modelStateErrors[fieldName];
          const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
          if (control) {
            // integrate into Angular's validation
            control.setErrors({
              modelStateError: { error: modelStateError }
            });
          } else {
            // for cross field validations -> show the validation error at the top of the screen
            this.errors.push(modelStateError);
          }
        }
      }
    } else {
      this.errors.push("something went wrong!");
    }
  }

  lowerCaseFirstLetter(data: string): string {
    return data.charAt(0).toLowerCase() + data.slice(1);
  }
توضیحات:
در اینجا از آرایه‌ی errors برای نمایش خطاهای عمومی در سطح فرم استفاده می‌کنیم. این خطاها در ModelState، دارای کلید مساوی "" هستند. به همین جهت حلقه‌ای را بر روی شیء responseError.error تشکیل می‌دهیم. به این ترتیب می‌توان به نام خواص و همچنین خطاهای متناظر با آن‌ها رسید.
 const control = form.controls[fieldName] || form.controls[this.lowerCaseFirstLetter(fieldName)];
از نام خاصیت یا فیلد، جهت یافتن کنترل متناظر با آن، در فرم جاری استفاده می‌کنیم. ممکن است کنترل تعریف شده camel case و یا pascal case باشد. به همین جهت دو حالت بررسی را در اینجا مشاهده می‌کنید.
در ادامه اگر control ایی یافت شد، توسط متد setErrors، کلید جدید modelStateError را که دارای خاصیت سفارشی error است، تنظیم می‌کنیم. با اینکار سبب خواهیم شد تا خطای اعتبارسنجی دریافتی از سمت سرور، با سیستم اعتبارسنجی Angular یکی شود. به این ترتیب می‌توان این خطا را دقیقا ذیل همین کنترل در فرم نمایش داد. اگر کنترلی یافت نشد (کلید آن "" بود و یا جزو نام کنترل‌های موجود در آرایه‌ی form.controls نبود)، این خطا را به آرایه‌ی errors اضافه می‌کنیم تا در بالاترین سطح فرم قابل نمایش شود.

نحوه‌ی استفاده‌ی از متد processModelStateErrors فوق را در متد submitForm، در قسمت شکست عملیات ارسال اطلاعات، مشاهده می‌کنید:
  model = new Movie("", "", 0, "");
  successfulSave: boolean;
  errors: string[] = [];

  constructor(private movieService: MovieService) { }

  ngOnInit() {
  }

  submitForm(form: NgForm) {
    console.log(form);

    this.errors = [];
    this.movieService.postMovieForm(this.model).subscribe(
      (data: Movie) => {
        console.log("Saved data", data);
        this.successfulSave = true;
      },
      (responseError: HttpErrorResponse) => {
        this.successfulSave = false;
        console.log("Response Error", responseError);
        this.processModelStateErrors(form, responseError);
      });
  }


نمایش خطاهای اعتبارسنجی عمومی فرم

اکنون که کار مقدار دهی آرایه‌ی errors انجام شده‌است، می‌توان حلقه‌ای را بر روی آن تشکیل داد و عناصر آن‌را در بالای فرم، به صورت عمومی و مستقل از تمام فیلدهای آن نمایش داد:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
  <div class="alert alert-danger" role="alert" *ngIf="errors.length > 0">
    <ul>
      <li *ngFor="let error of errors">
        {{ error }}
      </li>
    </ul>
  </div>
  <div class="alert alert-success" role="alert" *ngIf="successfulSave">
    Movie saved successfully!
  </div>



نمایش خطاهای اعتبارسنجی در سطح فیلدهای فرم

با توجه به تنظیم خطاهای اعتبارسنجی کنترل‌های Angular در متد processModelStateErrors و داشتن کلید جدید modelStateError
control.setErrors({
  modelStateError: { error: modelStateError }
});
اکنون می‌توان از این کلید جدید (ctrl.errors.modelStateError)، به صورت ذیل جهت نمایش خطای متناظر با آن (ctrl.errors.modelStateError.error) استفاده کرد:


<ng-template #validationErrorsTemplate let-ctrl="control">
  <div *ngIf="ctrl.invalid && ctrl.touched">
    <div class="alert alert-danger"  *ngIf="ctrl.errors.required">
      This field is required.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.minlength">
      This field should be minimum {{ctrl.errors.minlength.requiredLength}} characters.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.maxlength">
      This field should be max {{ctrl.errors.maxlength.requiredLength}} characters.
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.pattern">
      This field's pattern: {{ctrl.errors.pattern.requiredPattern}}
    </div>
    <div class="alert alert-danger"  *ngIf="ctrl.errors.modelStateError">
      {{ctrl.errors.modelStateError.error}}
    </div>
  </div>
</ng-template>
چون تکرار خطاهای اعتبارسنجی در ذیل هر فیلد، فرم را بیش از اندازه شلوغ می‌کند، می‌توان توسط یک ng-template این کدهای تکراری را تبدیل به یک قالب کرد و اکنون استفاده‌ی از این قالب، به سادگی فراخوانی یک ng-container است:
  <div class="form-group" [class.has-error]="releaseDate.invalid && releaseDate.touched">
    <label class="control-label" for="releaseDate">Release Date</label>
    <input type="text" name="releaseDate" #releaseDate="ngModel"  class="form-control"
      required [(ngModel)]="model.releaseDate" />
    <ng-container *ngTemplateOutlet="validationErrorsTemplate; context:{ control: releaseDate }"></ng-container>
  </div>
در اینجا در ngTemplateOutlet، ابتدا نام قالب متناظر ذکر می‌شود و سپس در context آن، نام خاصیت control را که توسط قالب دریافت می‌شود، به template reference variable متناظری تنظیم می‌کنیم، تا به کنترل جاری اشاره کند. به این ترتیب می‌توان به فرم‌هایی خلوت‌تر و با قابلیت مدیریت بهتری رسید.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
جایگزینی اسکریپت‌های WebResource.axd با فایل‌های استاتیک در ASP.NET Web forms
مدتی قبل سؤال مهمی در سایت مطرح شد، به شرح زیر:
«من از کنترل‌های تلریک استفاده می‌کنم که یک سری اسکریپت را بصورت
 http://localhost:1244/WebResource.axd?d=aklE6L8AEfPEgIS3T-oXc6mevPfbpi6VRp_ZTP2nBVrnt5ULOFYD3GNWRrDHwANC3VDQlL8dLAa5g35dzgHyuzAgAguIpYrf-_NXIJwNNu0YRSnH3-MgKMfnwKBKF_Lk2E5oeIcLL78uDlQ0se_GxQ2&t=635231470568640000 
به فرم تزریق می‌کند و بعضی وقت‌ها داخلش xp و یا یک سری دستورات اس‌کیوال تولید می‌شوند. در این حالت این مسیرها توسط ISA Server در شبکه داخلی حمله تشخیص داده شده و بلاک خواهند شد و عملا برنامه از کار می‌افتد. آیا راهی برای خلاصی از دست آن‌ها هست؟»

پاسخ: بلی. از دات نت 3 و نیم به بعد، امکان جایگزینی کامل اسکریپت‌های خودکار مدفون شده در اسمبلی‌ها با فایل‌های استاتیک پیش بینی شده‌است که در ادامه نحوه‌ی استخراج و کار با آن‌ها را بررسی خواهیم کرد.

الف) یافتن اسکریپت‌های مدفون در اسمبلی‌ها

در ابتدا اسمبلی حاوی کنترل‌های وب فرم مدنظر خود را باید توسط برنامه‌های Reflector یا ILSpy و امثال آن‌ها گشوده و نام دقیق منبع و همچنین محتوای آن فایل اسکریپت را استخراج کنید. برای مثال:


در این تصویر، اسمبلی استاندارد System.Web.Extensions مورد بررسی قرار گرفته است. برای نمونه اگر بخواهید اسکریپت‌های متناظر با ScriptManager و UpdatePanel را با معادل‌های استاتیک آن‌ها جایگزین کنید، باید دو فایل MicrosoftAjaxWebForms.js و MicrosoftAjax.js را از این اسمبلی استخراج نمائید. (برنامه‌های یاد شده امکان ذخیره سازی منابع را نیز می‌دهند)


ب) وادار کردن ASP.NET به استفاده از نسخه‌ی استاتیک منابع

    <asp:ScriptManager ID="Scriptmanager1" runat="server">
            <Scripts>
                <asp:ScriptReference Name="MicrosoftAjaxWebForms.js" Assembly="System.Web.Extensions"
                    Path="~/staticJS1.js" />
                <asp:ScriptReference Name="MicrosoftAjax.js" Assembly="System.Web.Extensions" Path="~/staticJS2.js" />
            </Scripts>
    </asp:ScriptManager>
فرض کنید دو اسکریپت یاد شده را در فایل‌های staticJS1.js و staticJS2.js در ریشه‌ی سایت خود ذخیره کرده‌اید. اکنون یک ScriptManager را به صفحه اضافه کرده و مطابق کدهای فوق، اسکریپت‌های مدفون شده در اسمبلی System.Web.Extensions را به این فایل‌های استاتیک هدایت کنید. همانطور که عنوان شد نام این مداخل باید دقیقا با نام موجود در اسمبلی یکی باشد؛ در غیر اینصورت با خطای ذیل مواجه خواهید شد:
 The assembly 'System.Web.Extensions' does not contain a Web resource that has the name 'xyz.js'.
Make sure that the resource name is spelled correctly.
Make sure that the application references the correct version of an ASP.NET AJAX Framework assembly.
اکنون اگر برنامه را اجرا کنید (با فرض قرار داشتن یک ScriptManager و UpdatePanel در صفحه)، اینبار دیگر در سورس صفحه، شاهد آدرس‌های طولانی WebResource.axd و ScriptResource.axd نخواهید بود. به صورت خودکار از دو فایل استاتیک تنظیم شده، استفاده می‌شود:
 <script src="staticJS1.js" type="text/javascript"></script>
<script src="staticJS2.js" type="text/javascript"></script>
بدیهی است در صورت نیاز، باید تعاریف سایر اسکریپت‌های مدفون در اسمبلی یاد شده یا اسمبلی System.Web را نیز به صفحه از طریق ScriptManager اضافه کرد. در مورد کنترل‌های ثالث نیز وضع به همین صورت است و استاندارد آن تفاوتی نمی‌کند.


یک نکته‌ی تکمیلی
در مطلب «ASP.NET 4.5 ScriptManager Improvements in WebForms » مشاهده خواهید کرد که از ASP.NET 4.5 به بعد، طی دو بسته‌ی نیوگت که هر از چندگاهی به روز می‌شوند، کلیه اسکریپت‌های System.Web و System.Web.Extensions خارج از این اسمبلی‌ها نیز قابل دریافت بوده و با استفاده از سیستم bunding & minification می‌توان آن‌ها را فشرده و یکی کرد.
مطالب
اعتبارسنجی از راه دور در فرم‌های مبتنی بر قالب‌های Angular
در پروژه angular2-validations، یک نمونه پیاده سازی اعتبارسنجی از راه دور یا RemoteValidation را می‌توانید مشاهده کنید. این پیاده سازی مبتنی بر Promiseها است. در مطلب جاری پیاده سازی دیگری را بر اساس Observableها مشاهده خواهید کرد و همچنین ساختار آن شبیه به ساختار remote validation در ASP.NET MVC و jQuery Validator طراحی شده‌است.


نگاهی به ساختار طراحی اعتبارسنجی از راه دور در ASP.NET MVC و jQuery Validator

در نگارش‌های مختلف ASP.NET MVC و ASP.NET Core، ویژگی Remote سمت سرور، سبب درج یک چنین ویژگی‌هایی در سمت کلاینت می‌شود:
data-val-remote="کلمه عبور وارد شده را راحت می&zwnj;توان حدس زد!" 
data-val-remote-additionalfields="*.Password1" 
data-val-remote-type="POST" 
data-val-remote-url="/register/checkpassword"
که شامل موارد ذیل است:
- متن نمایشی خطای اعتبارسنجی.
- تعدادی فیلد اضافی که در صورت نیز از فرم استخراج می‌شوند و به سمت سرور ارسال خواهند شد.
- نوع روش ارسال اطلاعات به سمت سرور.
- یک URL که مشخص می‌کند، این اطلاعات باید به کدام اکشن متد در سمت سرور ارسال شوند.

سمت سرور هم می‌تواند یک true یا false را بازگشت دهد و مشخص کند که آیا اطلاعات مدنظر معتبر نیستند یا هستند.
شبیه به یک چنین ساختاری را در ادامه با ایجاد یک دایرکتیو سفارشی اعتبارسنجی برنامه‌های Angular تدارک خواهیم دید.


ساختار اعتبارسنج‌های سفارشی async در Angular

در مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» جزئیات نوشتن اعتبارسنج‌های متداول فرم‌های Angular را بررسی کردیم. این نوع اعتبارسنج‌ها چون اطلاعاتی را به صورت Ajax ایی به سمت سرور ارسال نمی‌کنند، با پیاده سازی اینترفیس Validator تهیه خواهند شد:
 export class EmailValidatorDirective implements Validator {
اما زمانیکه نیاز است اطلاعاتی مانند نام کاربری یا ایمیل او را به سرور ارسال کنیم و در سمت سرور، پس از جستجوی در بانک اطلاعاتی، منحصربفرد بودن آن‌ها مشخص شود یا خیر، دیگر این روش همزمان پاسخگو نخواهد بود. به همین جهت اینبار اینترفیس دیگری به نام AsyncValidator برای انجام اعمال async و Ajax ایی در Angular تدارک دیده شده‌است:
 export class RemoteValidatorDirective implements AsyncValidator {
در این حالت امضای متد validate این اینترفیس به صورت ذیل است:
validate(c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;
یعنی در اینجا هم می‌توان یک Promise را بازگشت داد (مانند پیاده سازی که در ابتدای بحث عنوان شد) و یا می‌توان یک Observable را بازگشت داد که در ادامه نمونه‌ای از پیاده سازی این روش دوم را بررسی می‌کنیم؛ چون امکانات بیشتری را نسبت به Promiseها به همراه دارد. برای مثال در اینجا می‌توان اندکی صبر کرد تا کاربر تعدادی حرف را وارد کند و سپس این اطلاعات را به سرور ارسال کرد. به این ترتیب ترافیک ارسالی به سمت سرور کاهش پیدا می‌کند.


پیاده سازی یک اعتبارسنج از راه دور مبتنی بر Observableها در Angular

ابتدا یک دایرکتیو جدید را به نام RemoteValidator به ماژول custom-validators اضافه کرده‌ایم:
 >ng g d CustomValidators/RemoteValidator -m custom-validators.module
در ادامه کدهای کامل این اعتبارسنج را مشاهده می‌کنید:
import { Directive, Input } from "@angular/core";
import {
  AsyncValidator,
  AbstractControl,
  NG_ASYNC_VALIDATORS
} from "@angular/forms";
import { Http, RequestOptions, Response, Headers } from "@angular/http";
import "rxjs/add/operator/map";
import "rxjs/add/operator/distinctUntilChanged";
import "rxjs/add/operator/takeUntil";
import "rxjs/add/operator/take";
import { Observable } from "rxjs/Observable";
import { Subject } from "rxjs/Subject";

@Directive({
  selector:
    "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: RemoteValidatorDirective,
      multi: true
    }
  ]
})
export class RemoteValidatorDirective implements AsyncValidator {
  @Input("remote-url") remoteUrl: string;
  @Input("remote-field") remoteField: string;
  @Input("remote-additional-fields") remoteAdditionalFields: string;

  constructor(private http: Http) {}

  validate(control: AbstractControl): Observable<{ [key: string]: any }> {
    if (!this.remoteUrl || this.remoteUrl === undefined) {
      return Observable.throw("`remoteUrl` is undefined.");
    }

    if (!this.remoteField || this.remoteField === undefined) {
      return Observable.throw("`remoteField` is undefined.");
    }

    const dataObject = {};
    if (
      this.remoteAdditionalFields &&
      this.remoteAdditionalFields !== undefined
    ) {
      const otherFields = this.remoteAdditionalFields.split(",");
      otherFields.forEach(field => {
        const name = field.trim();
        const otherControl = control.root.get(name);
        if (otherControl) {
          dataObject[name] = otherControl.value;
        }
      });
    }

    // This is used to signal the streams to terminate.
    const changed$ = new Subject<any>();
    changed$.next(); // This will signal the previous stream (if any) to terminate.

    const debounceTime = 400;

    return new Observable((obs: any) => {
      control.valueChanges
        .takeUntil(changed$)
        .take(1)
        .debounceTime(debounceTime)
        .distinctUntilChanged()
        .flatMap(term => {
          dataObject[this.remoteField] = term;
          return this.doRemoteValidation(dataObject);
        })
        .subscribe(
          (result: IRemoteValidationResult) => {
            if (result.result) {
              obs.next(null);
            } else {
              obs.next({
                remoteValidation: {
                  remoteValidationMessage: result.message
                }
              });
            }

            obs.complete();
          },
          error => {
            obs.next(null);
            obs.complete();
          }
        );
    });
  }

  private doRemoteValidation(data: any): Observable<IRemoteValidationResult> {
    const headers = new Headers({ "Content-Type": "application/json" }); // for ASP.NET MVC
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(this.remoteUrl, JSON.stringify(data), options)
      .map(this.extractData)
      .do(result => console.log("remoteValidation result: ", result))
      .catch(this.handleError);
  }

  private extractData(res: Response): IRemoteValidationResult {
    const body = <IRemoteValidationResult>res.json();
    return body || (<IRemoteValidationResult>{});
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }
}

export interface IRemoteValidationResult {
  result: boolean;
  message: string;
}
توضیحات تکمیلی

ساختار Directive تهیه شده مانند همان مطلب «نوشتن اعتبارسنج‌های سفارشی برای فرم‌های مبتنی بر قالب‌ها در Angular» است، تنها با یک تفاوت:
@Directive({
  selector:
    "[appRemoteValidator][formControlName],[appRemoteValidator][formControl],[appRemoteValidator][ngModel]",
  providers: [
    {
      provide: NG_ASYNC_VALIDATORS,
      useExisting: RemoteValidatorDirective,
      multi: true
    }
  ]
})
در اینجا بجای NG_VALIDATORS، از NG_ASYNC_VALIDATORS استفاده شده‌است.

سپس ورودی‌های این دایرکتیو را مشاهده می‌کنید:
export class RemoteValidatorDirective implements AsyncValidator {
  @Input("remote-url") remoteUrl: string;
  @Input("remote-field") remoteField: string;
  @Input("remote-additional-fields") remoteAdditionalFields: string;
به این ترتیب زمانیکه appRemoteValidator به المانی اضافه می‌شود (نام selector این دایرکتیو)، سبب فعالسازی این اعتبارسنج می‌گردد.
<input #username="ngModel" required maxlength="8" minlength="4" type="text"
        appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
        remote-additional-fields="email,password" class="form-control" name="username"
        [(ngModel)]="model.username">
- در اینجا توسط ویژگی remote-url، آدرس اکشن متد سمت سرور دریافت می‌شود.
- ویژگی remote-field مشخص می‌کند که اطلاعات المان جاری با چه کلیدی به سمت سرور ارسال شود.
- ویژگی remote-additional-fields مشخص می‌کند که علاوه بر اطلاعات کنترل جاری، اطلاعات کدامیک از کنترل‌های دیگر را نیز می‌توان به سمت سرور ارسال کرد.

یک نکته:
ذکر "remote-field="FirstName به معنای انتساب مقدار رشته‌ای FirstName به خاصیت متناظر با ویژگی remote-field است.
انتساب ویژه‌ی "remoteUsernameValidationUrl" به [remote-url]، به معنای انتساب مقدار متغیر remoteUsernameValidationUrl که در کامپوننت متناظر این قالب مقدار دهی می‌شود، به خاصیت متصل به ویژگی remote-url است.
export class UserRegisterComponent implements OnInit {
   remoteUsernameValidationUrl = "api/Employee/CheckUser";
بنابراین اگر remote-field را نیز می‌خواستیم به همین نحو تعریف کنیم، ذکر '' جهت مشخص سازی انتساب یک رشته، ضروری می‌بود؛ یعنی درج آن به صورت:
 [remote-field]="'FirstName'"


ساختار مورد انتظار بازگشتی از سمت سرور

در کدهای فوق، یک چنین ساختاری باید از سمت سرور بازگشت داده شود:
export interface IRemoteValidationResult {
   result: boolean;
   message: string;
}
برای نمونه این ساختار را می‌توان توسط یک anonymous object ایجاد کرد و بازگشت داد:
namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class EmployeeController : Controller
    {
        [HttpPost("[action]")]
        [ResponseCache(Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult CheckUser([FromBody] Employee model)
        {
            var remoteValidationResult = new { result = true, message = $"{model.FirstName} is fine!" };
            if (model.FirstName?.Equals("Vahid", StringComparison.OrdinalIgnoreCase) ?? false)
            {
                remoteValidationResult = new { result = false, message = "username:`Vahid` is already taken." };
            }

            return Json(remoteValidationResult);
        }
    }
}
در اینجا برای مثال بررسی می‌شود که آیا FirstName ارسالی از سمت کاربر، معادل Vahid است یا خیر؟ اگر بله، result به false تنظیم شده و همچنین پیام خطایی نیز بازگشت داده می‌شود.
همچنین اعتبارسنج سفارشی از راه دور فوق، پیام‌ها را تنها از طریق HttpPost ارسال می‌کند. علت اینجا است که در حالت POST، برخلاف حالت GET می‌توان اطلاعات بیشتری را بدون نگرانی از طول URL، ارسال کرد و همچنین کل درخواست، به علت وجود کاراکترهای غیرمجاز در URL (حالت GET، به درخواست یک URL از سرور تفسیر می‌شود)، برگشت نمی‌خورد.


تکمیل کامپوننت فرم ثبت نام کاربران

در ادامه تکمیل قالب user-register.component.html را مشاهده می‌کنید:
    <div class="form-group" [class.has-error]="username.invalid && username.touched">
      <label class="control-label">User Name</label>
      <input #username="ngModel" required maxlength="8" minlength="4" type="text"
        appRemoteValidator [remote-url]="remoteUsernameValidationUrl" remote-field="FirstName"
        remote-additional-fields="email,password" class="form-control" name="username"
        [(ngModel)]="model.username">
      <div *ngIf="username.pending" class="alert alert-warning">
        Checking server, Please wait ...
      </div>
      <div *ngIf="username.invalid && username.touched">
        <div class="alert alert-danger"  *ngIf="username.errors.remoteValidation">
          {{username.errors.remoteValidation.remoteValidationMessage}}
        </div>
      </div>
    </div>
در مورد ویژگی‌های appRemoteValidator پیشتر بحث شد. در اینجا تنها یک نکته‌ی جدید وجود دارد:
زمانیکه یک async validator مشغول به کار است و هنوز پاسخی را دریافت نکرده‌است، خاصیت pending را به true تنظیم می‌کند. به این ترتیب می‌توان پیام اتصال به سرور را نمایش داد:


همچنین چون در اینجا نحوه‌ی طراحی شکست اعتبارسنجی به صورت ذیل است:
obs.next({
                remoteValidation: {
                  remoteValidationMessage: result.message
                }
              });
وجود کلید remoteValidation در مجموعه‌ی username.errors، بیانگر وجود خطای اعتبارسنجی از راه دور است و به این ترتیب می‌توان پیام دریافتی از سمت سرور را نمایش داد:



مزایای استفاده از Observableها در حین طراحی async validators

در کدهای فوق چنین مواردی را هم مشاهده می‌کنید:
    // This is used to signal the streams to terminate.
    const changed$ = new Subject<any>();
    changed$.next(); // This will signal the previous stream (if any) to terminate.

    const debounceTime = 400;

    return new Observable((obs: any) => {
      control.valueChanges
        .takeUntil(changed$)
        .take(1)
        .debounceTime(debounceTime)
        .distinctUntilChanged()
در اینجا بجای کار مستقیم با control.value (روش متداول دسترسی به مقدار کنترل دریافتی در یک اعتبارسنج)، به رخ‌داد valueChanges آن متصل شده و سپس پس از 400 میلی‌ثانیه، جمع نهایی ورودی کاربر، در اختیار متد http.post برای ارسال به سمت سرور قرار می‌گیرد. به این ترتیب می‌توان تعداد رفت و برگشت‌های به سمت سرور را کاهش داد و به ازای هر یکبار فشرده شدن دکمه‌ای توسط کاربر، سبب بروز یکبار رفت و برگشت به سرور نشد.
همچنین وجود و تعریف new Subject، دراینجا ضروری است و از نشتی حافظه و همچنین رفت و برگشت‌های اضافه‌ی دیگری به سمت سرور، جلوگیری می‌کند. این subject سبب می‌شود تا کلیه اعمال ناتمام پیشین، لغو شده (takeUntil) و تنها آخرین درخواست جدید رسیده‌ی پس از 400 میلی‌ثانیه، به سمت سرور ارسال شود.

بنابراین همانطور که مشاهده می‌کنید، Observableها فراتر هستند از صرفا ارسال اطلاعات به سرور و بازگشت آن‌ها به سمت کلاینت (استفاده‌ی متداولی که از آن‌ها در برنامه‌های Angular وجود دارد).


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
نحوه استفاده از افزونه Firebug برای دیباگ برنامه‌های ASP.NET مبتنی بر jQuery
هر از چندگاهی سؤال «این مثال jQuery رو نمی‌تونم اجرا یا باز سازی کنم» در این سایت یا سایت‌های مشابه تکرار می‌شوند. بنابراین بهتر است نحوه عیب یابی برنامه‌های ASP.NET مبتنی بر jQuery را یکبار با هم مرور کنیم. در اینجا، مثال تهیه یک Image Slider را که پیشتر در سایت مطرح شده است، به نحوی دیگر بررسی خواهیم کرد:
1) فراموش می‌کنیم تا اسکریپت اصلی jQuery را به درستی پیوست و مسیردهی کنیم.
2) مسیر Generic handler دیگری را ذکر می‌کنیم.
3) مسیرهای تصاویری را که Image slider باید نمایش دهد، کاملا بی‌ربط ذکر می‌کنیم.
4) خروجی JSON نامربوطی را بازگشت می‌دهیم.
5) یکبار هم یک استثنای عمدی دستی را در بین کدها قرار خواهیم داد.

و ... بعد سعی می‌کنیم با استفاده از Firebug عیوب فوق را یافته و اصلاح کنیم؛ تا به یک برنامه قابل اجرا برسیم.


معرفی برنامه‌ای که کار نمی‌کند!

یک برنامه ASP.NET Empty web application را آغاز کنید. سپس سه پوشه Scripts، Content و Images را به آن اضافه نمائید. در این پوشه‌ها، اسکریپت‌های نمایش دهنده تصاویر، Css آن و تصاویری که قرار است نمایش داده شوند، قرار می‌گیرند:


سپس یک فایل default.aspx و یک فایل OrbitHandler.ashx را نیز به پروژه با محتویات ذیل اضافه کنید: (در این دو فایل، 5 مورد مشکل ساز یاد شده لحاظ شده‌اند)
محتویات فایل OrbitHandler.ashx.cs مطابق کدهای ذیل است:
using System.Collections.Generic;
using System.IO;
using System.Web;
using System.Web.Script.Serialization;

namespace OrbitWebformsTest
{
    public class Picture
    {
        public string Title { set; get; }
        public string Path { set; get; }
    }

    public class OrbitHandler : IHttpHandler
    {
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = /*"Images/" + name*/ name,
                    Title = name
                });
            }

            return results;
        }

        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = /*new JavaScriptSerializer().Serialize(items)*/ string.Empty;
            throw new InvalidDataException("همینطوری");
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }

        public bool IsReusable
        { 
            get { return false; } 
        }
    }
}
در اینجا جهت سهولت دموی برنامه (و همچنین امکان باز تولید آن توسط خوانندگان)، از بانک اطلاعاتی استفاده نشده و عمدا از یک لیست جنریک تشکیل شده در حافظه کمک گرفته شده است. تصاویر برنامه در پوشه Images واقع در ریشه سایت، قرار دارند. بنابراین توسط متد PicturesDataSource، فایل‌های این پوشه را یافته و مطابق ساختار کلاس Picture بازگشت می‌دهیم. نهایتا این اطلاعات به ظاهر قرار است با فرمت JSON بازگشت داده شوند تا بتوان نتیجه را توسط افزونه Orbit استفاده کرد.

همچنین کدهای صفحه ASPX ایی که قرار است (به ظاهر البته) از این Generic handler استفاده کند به نحو ذیل است:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="default.aspx.cs" Inherits="OrbitWebformsTest._default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <link href="Content/orbit-1.2.3.css" rel="stylesheet" type="text/css" />
    <script src="Script/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.orbit-1.2.3.min.js" type="text/javascript"></script>
</head>
<body>
    <form id="form1" runat="server">
    <div id="featured">
    </div>
    </form>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                url: "Handler.ashx",
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    $.each(data, function (i, b) {
                        var str = '<img src="' + b.Path + '" alt="' + b.Title + '"/>';
                        $("#featured").append(str);
                    });
                    $('#featured').orbit();
                },
                dataType: "json"
            });
        });
    </script>
</body>
</html>
خوب! اگر پروژه را اجرا کنیم، کار نمی‌کند. یک مستطیل مشکی رنگ در کنار صفحه ظاهر شده و همین! حالا چکار باید کرد؟


مراحل عیب یابی برنامه‌ای که کار نمی‌کند!

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


اولین موردی که مشهود است، نمایش عدد 3، کنار آیکن فایرباگ می‌باشد. این عدد به معنای وجود خطاهای اسکریپتی در کدهای ما است.
برای مشاهده این خطاها، بر روی برگه Console آن کلیک کنید: 


بله. مشخص است که مسیر دهی فایل jquery-1.5.1.min.js صحیح نبوده و همین مساله سبب بروز خطاهای اسکریپتی گردیده است. برای اصلاح آن سطر زیر را در برنامه تغییر دهید:
 <script src="Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
پیشتر پوشه Script ذکر شده بود که باید تبدیل به Scripts شود.

مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید. اینبار در برگه Console و یا در برگه شبکه فایرباگ، خطای یافت نشدن Generic handler نمایان می‌شوند:


برای رفع آن به فایل default.aspx مراجعه و بجای معرفی Handler.ashx، نام OrbitHandler.ashx را وارد کنید.
مجددا دکمه F5 را فشرده و سایت را با تنظیمات جدید اجرا کنید.


اگر به برگه کنسول دقت کنیم، بروز استثناء در کدها تشخیص داده شده و همچنین در برگه Response پاسخ دریافتی از سرور، جزئیات صفحه خطای بازگشتی از آن نیز قابل بررسی و مشاهده است.
اینبار به فایل OrbitHandler.ashx.cs مراجعه کرده و سطر throw new InvalidDataException را حذف می‌کنیم. در ادامه برنامه را کامپایل و مجددا اجرا خواهیم کرد.



با اجرای مجدد سایت، تبادل اطلاعات صحیحی با فایل OrbitHandler.ashx برقرار شده است، اما خروجی خاصی قابل مشاهده نیست. بنابراین بازهم سایت کار نمی‌کند.
برای رفع این مشکل، متد ProcessRequest را به نحو ذیل تغییر خواهیم داد:
        public void ProcessRequest(HttpContext context)
        {
            var items = PicturesDataSource();
            var json = new JavaScriptSerializer().Serialize(items);            
            context.Response.ContentType = "text/plain";
            context.Response.Write(json);
        }
برنامه را کامپایل کرده و اجرا می‌کنیم. برنامه اجرا می‌شود، اما باز هم کار نمی‌کند. مشکل از کجاست؟


بله. تمام تنظیمات به نظر درست هستند، اما در برگه شبکه فایرباگ تعدادی خطای 404 و یا «یافت نشد»، مشاهده می‌شوند. مشکل اینجا است که مسیرهای بازگشت داده شده توسط متد Directory.GetFiles، مسیرهای مطلقی هستند؛ مانند c:\path\images\01.jpg و جهت نمایش در یک وب سایت مناسب نمی‌باشند. برای تبدیل آن‌ها به مسیرهای نسبی، اینبار کدهای متد تهیه منبع داده را به نحو ذیل ویرایش می‌کنیم:
        IList<Picture> PicturesDataSource()
        {
            var results = new List<Picture>();
            var path = HttpContext.Current.Server.MapPath("~/Images");

            foreach (var item in Directory.GetFiles(path, "*.*"))
            {
                var name = Path.GetFileName(item);
                results.Add(new Picture
                {
                    Path = "Images/" + name,
                    Title = name
                });
            }

            return results;
        }
در این کدها فقط قسمت Path ویرایش شده است تا به مسیر پوشه Images واقع در ریشه سایت اشاره کند.
اینبار اگر برنامه را اجرا کنیم، بدون مشکل کار خواهد کرد.

بنابراین در اینجا مشاهده کردیم که اگر «برنامه‌ای مبتنی بر jQuery کار نمی‌کند»، چگونه باید قدم به قدم با استفاده از فایرباگ و امکانات آن، به خطاهایی که گزارش می‌دهد و یا مسیرهایی را که یافت نشد بیان می‌کند، دقت کرد تا بتوان برنامه را عیب یابی نمود.


سؤال مهم: اجرای کدهای jQuery Ajax فوق، چه تغییری را در صفحه سبب می‌شوند؟

اگر به برگه اسکریپت‌ها در کنسول فایرباگ مراجعه کنیم، امکان قرار دادن breakpoint بر روی سطرهای کدهای جاوا اسکریپتی نمایش داده شده نیز وجود دارد:


در اینجا همانند VS.NET می‌توان برنامه را در مرورگر اجرا کرده و تگ‌های تصویر پویای تولید شده را پیش از اضافه شدن به صفحه، مرحله به مرحله بررسی کرد. به این ترتیب بهتر می‌توان دریافت که آیا src بازگشت داده شده از سرور فرمت صحیحی دارد یا خیر و آیا به محل مناسبی اشاره می‌کند یا نه. همچنین در برگه HTML آن، عناصر پویای اضافه شده به صفحه نیز بهتر مشخص هستند:

مطالب
فعال سازی و پردازش Inline Add در jqGrid
پیشنیازها
فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid


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


فعال سازی افزودن، ویرایش و حذف Inline


فعال سازی ویرایش و حذف Inline را پیشتر نیز بررسی کرده بودیم. تنها کافی است یک ستون جدید را با 'formatter: 'actions تعریف کنیم. به صورت خودکار، دکمه‌ی ویرایش، حذف، ذخیره سازی و لغو Inline ظاهر می‌شوند و همچنین بدون نیاز به کدنویسی بیشتری کار می‌کنند.
اما در کدهای ذیل اندکی این ستون را سفارشی سازی کرده‌ایم. در قسمت formatter آن، دکمه‌های edit و delete یک سطر جدید توکار اضافه شده را حذف کرده‌ایم. زیرا در این حالت خاص، وجود این دکمه‌ها ضروری نیستند. بهتر است در این حالت دکمه‌‌های save و cancel ظاهر شوند:
            $('#list').jqGrid({
                caption: "آزمایش نهم",
                // ....
                colModel: [
                    {
                        name: 'myac', width: 80, fixed: true, sortable: false,
                        resize: false,
                        //formatter: 'actions',
                        formatter: function (cellvalue, options, rowObject) {
                            if (cellvalue === undefined && options.rowId === "_empty") {
                                // در حالت نمایش ردیف توکار جدید دکمه‌های ویرایش و حذف معنی ندارند
                                options.colModel.formatoptions.editbutton = false;
                                options.colModel.formatoptions.delbutton = false;
                            }
                            return $.fn.fmatter.actions(cellvalue, options, rowObject);
                        },
                        formatoptions: {
                            keys: true,
                            afterSave: function (rowid, response) {
                            },
                            delbutton: true,
                            delOptions: {
                                url: "@Url.Action("DeleteUser", "Home")"
                            }
                        }
                    }
                ],
                //...
            }).navGrid(
                '#pager',
//...
)
                .jqGrid('gridResize', { minWidth: 400, minHeight: 150 })
                .jqGrid('inlineNav', '#pager',
                {
                    edit: true, add: true, save: true, cancel: true,
                    edittext: "ویرایش", addtext: "جدید", savetext: "ذخیره", canceltext: "لغو",
                    addParams: {
                        // اگر می‌خواهید ردیف‌های جدید در ابتدا ظاهر شوند، این سطر را حذف کنید
                        position: "last", //ردیف‌های جدید در آخر ظاهر می‌شوند
                        rowID: '_empty',
                        useDefValues: true,
                        addRowParams: getInlineNavParams(true)
                    },
                    editParams: getInlineNavParams(false)
                });
قسمتی که کار فعال سازی Inline Add را انجام می‌دهد، تعریف مرتبط با inlineNav است که به انتهای تعاریف متداول گرید اضافه شده‌است.
در اینجا 4 دکمه‌ی ویرایش، جدید، ذخیره و لغو، در نوار pager پایین گرید ظاهر خواهند شد (سمت چپ؛ سمت راست همان دکمه‌‌های نمایش فرم‌های پویا هستند).


سپس باید دو قسمت مهم addParams و editParams آن‌را مقدار دهی کرد.
در قسمت addParams، مشخص می‌کنیم که ID ردیف اضافه شده، مساوی کلمه‌ی _empty باشد. اگر به کدهای formatter ستون action دقت کنید، از این ID برای تشخیص افزوده شدن یک ردیف جدید استفاده شده‌است.
position در اینجا به معنای محل افزوده شدن یک ردیف خالی است. مقدار پیش فرض آن first است؛ یعنی همیشه در اولین ردیف گرید، این ردیف جدید اضافه می‌شود. در اینجا به last تنظیم شده‌است تا در پایین گرید و پس از رکوردهای موجود، نمایش داده شود.
useDefValues سبب استفاده از مقادیر پیش فرض تعریف شده در ستون‌های گرید در حین افزوده شدن یک ردیف جدید می‌گردد.
addRowParams و editParams هر دو ساختار تقریبا یکسانی دارند که به نحو ذیل تعریف می‌شوند:
        function getInlineNavParams(isAdd) {
            return {
                // استفاده از آدرس‌های مختلف برای حالات ویرایش و ثبت اطلاعات جدید
                url: isAdd ? '@Url.Action("AddUser", "Home")' : '@Url.Action("EditUser","Home")',
                key: true,
                restoreAfterError: false, // این مورد سبب می‌شود تا اعتبارسنجی سمت سرور قابل اعمال شود
                oneditfunc: function (rowId) {
                    // نمایش دکمه‌های ذخیره و لغو داخل همان سطر
                    $("#jSaveButton_" + rowId).show();
                    $("#jCancelButton_" + rowId).show();
                },
                successfunc: function () {
                    var $self = $(this);
                    setTimeout(function () {
                        $self.trigger("reloadGrid"); // دریافت کلید اصلی ردیف از سرور
                    }, 50);
                },
                errorfunc: function (rowid, response, stat) {
                    if (stat != 'error') // this.Response.StatusCode == 200
                        return;

                    var result = $.parseJSON(response.responseText);
                    if (result.success === false) {
                        //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                        $.jgrid.info_dialog($.jgrid.errors.errcap,
                            '<div class="ui-state-error">' + result.message + '</div>',
                            $.jgrid.edit.bClose,
                            { buttonalign: 'center' });
                    }
                }
            };
        }
در ابتدای کار مشخص می‌کنیم که آدرس‌های ذخیره سازی اطلاعات در سمت سرور برای حالت‌های Add و Edit کدام‌اند.
تنظیم restoreAfterError به false بسیار مهم است. اگر در سمت سرور خطای اعتبارسنجی گزارش شود و restoreAfterError مساوی true باشد (مقدار پیش فرض)، کاربر مجبور خواهد شد اطلاعات را دوباره وارد کند.
در روال رویدادگران oneditfunc دکمه‌ی save و cancel ردیف را که مخفی هستند، ظاهر می‌کنیم (مکمل formatter ستون action است).
در قسمت successfunc، پس از پایان موفقیت آمیز کار، متد reloadGrid را فراخوانی می‌کنیم. اینکار سبب می‌شود تا Id واقعی رکورد، از سمت سرور دریافت شود. از این Id برای ویرایش و همچنین حذف، استفاده خواهد شد. علت استفاده از setTimeout در اینجا این است که اندکی به DOM فرصت داده شود تا کارش به پایان برسد.
در قسمت errorfunc خطاهای اعتبارسنجی سفارشی سمت سرور را می‌توان دریافت و سپس توسط متد توکار info_dialog به کاربر نمایش داد.



یک نکته‌ی مهم در مورد ارسال خطاهای اعتبارسنجی از سمت سرور در حالت Inline Add

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                this.Response.StatusCode = 500; //این مورد برای افزودن داخل ردیف‌های گرید لازم است
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }
روال رویداد گردان errorfunc، اگر مقدار StatusCode بازگشتی از سمت سرور مساوی 200 باشد (حالت عادی و موفقیت آمیز)، مقدار stat مساوی error را باز نمی‌گرداند. به همین جهت است که در کدهای فوق، مقدار دهی this.Response.StatusCode را به 500 مشاهده می‌کنید. هر عددی غیر از 200 در اینجا به error تفسیر می‌شود. همچنین اگر این StatusCode سمت سرور تنظیم نشود، گرید فرض را بر موفقیت آمیز بودن عملیات گذاشته و successfunc را فراخوانی می‌کند.



مدیریت StatusCodeهای غیر از 200 در حالت کار با فرم‌های jqGrid

اگر هر دو حالت Inline Add و فرم‌های پویا را فعال کرده‌اید، بازگشت StatusCode = 500 سبب می‌شود تا دیگر نتوان خطاهای سفارشی سمت سرور را در بالای فرم‌ها به کاربر نمایش داد و در این حالت تنها یک internal server error را مشاهده خواهند کرد. برای رفع این مشکل فقط کافی است روال رویدادگران errorTextFormat را مدیریت کرد:
            $('#list').jqGrid({
                caption: "آزمایش نهم",
                //.........
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    //......... 
                    errorTextFormat: serverErrorTextFormat
                },
                //add options
                {
                    //......... 
                    errorTextFormat: serverErrorTextFormat
                },
                //delete options
                {
                    //......... 
                })
                .jqGrid('gridResize', { minWidth: 400, minHeight: 150 })
                .jqGrid('inlineNav', '#pager',
                {
                    //......... 
                });

        function serverErrorTextFormat (response) {
            // در حالتیکه وضعیت خروجی از سرور 200 نیست فراخوانی می‌شود
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                return result.message;
            }
            return "لطفا ورودی‌های وارد شده را بررسی کنید";
        }
errorTextFormat تنها در حالتیکه StatusCode بازگشتی از طرف سرور مساوی 200 نیست، فراخوانی می‌شود. در اینجا می‌توان response دریافتی را آنالیز و سپس پیام خطای سفارشی آن‌را جهت نمایش در فرم‌های پویای گرید، بازگشت داد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
jqGrid09.zip
مطالب
استفاده از API ترجمه گوگل

مطابق Ajax API ترجمه گوگل، برای ترجمه یک متن باید محتویات آدرس زیر را تحلیل کرد:
http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}
که در آن پارامتر اول، متن مورد نظر، پارامترهای 1 و 2 زبان‌های مبدا و مقصد می‌باشند. برای دریافت اطلاعات، ذکر ارجاع دهنده الزامی است (referrer)، اما ذکر کلید API گوگل اختیاری می‌باشد (که هر فرد می‌تواند کلید خاص خود را از گوگل دریافت کند).
بنابراین برای استفاده از آن تنها کافی است این URL را تشکیل داده و سپس محتویات خروجی آن‌را آنالیز کرد. فرمت نهایی دریافت شده از نوع JSON است. برای مثال اگر hello world! را به این سرویس ارسال نمائیم،‌ خروجی نهایی JSON‌ دریافت شده به صورت زیر خواهد بود:

//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}

در کتابخانه‌ی System.Web.Extensions.dll دات نت فریم ورک سه و نیم، کلاس JavaScriptSerializer برای این منظور پیش بینی شده است. تنها کافی است به متد Deserialize آن، متن JSON دریافتی را پاس کنیم:

GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);

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

//ResponseData.cs file
public class ResponseData
{
public string translatedText { get; set; }
}

//GoogleAjaxResponse.cs file
using System.Net;

/// <summary>
/// کلاسی جهت نگاشت اطلاعات جی سون دریافتی به آن
/// </summary>
public class GoogleAjaxResponse
{
public ResponseData responseData { get; set; }
public object responseDetails { get; set; }
public HttpStatusCode responseStatus { get; set; }
}
با این توضیحات، کلاس نهایی ترجمه گوگل ما به شکل زیر خواهد بود:

using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Web;
using System.Web.Script.Serialization;

//{\"responseData\": {\"translatedText\":\"سلام جهان!\"}, \"responseDetails\": null, \"responseStatus\": 200}

public class CGoogleTranslator
{
#region Fields (1)

/// <summary>
/// ارجاع دهنده
/// </summary>
private readonly string _referrer;

#endregion Fields

#region Constructors (1)

/// <summary>
/// مطابق مستندات نیاز به یک ارجاع دهنده اجباری می‌باشد
/// </summary>
/// <param name="referrer"></param>
public CGoogleTranslator(string referrer)
{
_referrer = referrer;
}

#endregion Constructors

#region Properties (2)

/// <summary>
/// ترجمه از زبان
/// </summary>
public CultureInfo FromLanguage { get; set; }

/// <summary>
/// ترجمه به زبان
/// </summary>
public CultureInfo ToLanguage { get; set; }

#endregion Properties

#region Methods (2)

// Public Methods (1)

/// <summary>
/// ترجمه متن با استفاده از موتور ترجمه گوگل
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public string TranslateText(string data)
{
//ساخت و انکدینگ آدرس مورد نظر
string url =
string.Format(
"http://ajax.googleapis.com/ajax/services/language/translate?v=1.0&q={0}&langpair={1}|{2}",
HttpUtility.UrlEncode(data), //needs a ref. to System.Web.dll
FromLanguage.TwoLetterISOLanguageName,
ToLanguage.TwoLetterISOLanguageName
);

//دریافت اطلاعات جی سون از گوگل
string jsonGoogleAjaxResponse = fetchWebPage(url);

//needs a ref. to System.Web.Extensions.dll
//نگاشت اطلاعات جی سون دریافت شده به کلاس مرتبط
GoogleAjaxResponse result =
new JavaScriptSerializer().Deserialize<GoogleAjaxResponse>(jsonGoogleAjaxResponse);

if (result != null && result.responseData != null && result.responseStatus == HttpStatusCode.OK)
{
return result.responseData.translatedText;
}
return string.Empty;
}
// Private Methods (1)

/// <summary>
/// دریافت محتویات جی سون بازگشتی از گوگل
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
string fetchWebPage(string url)
{
try
{
var uri = new Uri(url);
if (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)
{
var request = WebRequest.Create(uri) as HttpWebRequest;
if (request != null)
{
request.Method = WebRequestMethods.Http.Get;
request.Referer = _referrer;
request.UserAgent = "Mozilla/5.0 (Windows; U; Windows NT 5.0; ; rv:1.8.0.7) Gecko/20060917 Firefox/1.9.0.1";
request.AllowAutoRedirect = true;
request.Timeout = 1000 * 300;
request.KeepAlive = false;
request.ReadWriteTimeout = 1000 * 300;
request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;

using (var response = request.GetResponse() as HttpWebResponse)
{
if (response != null)
{
using (var reader = new StreamReader(response.GetResponseStream()))
{
return reader.ReadToEnd().Trim();
}
}
}
}
}
return string.Empty;
}
catch (Exception ex)
{
Console.WriteLine(String.Format("fetchWebPage: {0} >> {1}", ex.Message, url), true);
return string.Empty;
}
}

#endregion Methods
}
مثالی در مورد نحوه‌ی استفاده از آن برای ترجمه یک متن از انگلیسی به فارسی:

string res = new CGoogleTranslator("https://www.dntips.ir/")
{
FromLanguage = CultureInfo.GetCultureInfo("en-US"),
ToLanguage = CultureInfo.GetCultureInfo("fa-IR")
}.TranslateText("Hello world!");

نظرات مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
تفاوتی نمی‌کند. افزونه‌ی مورد استفاده وابستگی خاصی به ASP.NET MVC ندارد. خلاصه عملیات فوق به این نحو است که فراخوانی این افزونه در زمان submit فرم انجام می‌شود. همین رویداد را در فرم‌های مودال یا هر فرم دیگری نیز می‌توان تحت کنترل قرار داد و بازنویسی کرد.
مطالب
وادار کردن IIS به استفاده از ASP.Net 3.5

همانطور که مطلع هستید در تنظیمات یک دایرکتوری مجازی در IIS6 یا 5، حتی پس از نصب دات نت فریم ورک سه و نیم، گزینه انتخاب نگارش 3.5 ظاهر نمی‌شود و همان تنظیمات ASP.Net 2.0 کافی است (شکل زیر) (دات نت 3 و سه و نیم را می‌توان بعنوان افزونه‌هایی با مقیاس سازمانی (WF ، WCF و ...) برای دات نت 2 درنظر گرفت).




هنگام استفاده از VS.Net 2008 و تنظیم نوع پروژه به دات نت فریم ورک 3.5 ، به صورت خودکار تنظیمات لازم به وب کانفیگ برنامه جهت استفاده از کامپایلرهای مربوطه نیز اضافه می‌شوند که شاید از نظر دور بمانند.
برای آزمایش این مورد، فرض کنید صفحه زیر را بدون استفاده از code behind و VS.Net ایجاد کرده ایم (جهت آزمایش سریع یک قطعه کد Linq ).

<%@ Page Language="C#" %>

<%@ Import Namespace="System" %>
<%@ Import Namespace="System.Linq" %>

<form id="Form1" method="post" runat="server">
<asp:GridView ID="GridView1" runat="server" />
</form>


<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
string[] cities = {
"London", "Amsterdam", "San Francisco", "Las Vegas",
"Boston", "Raleigh", "Chicago", "Charlestown",
"Helsinki", "Nice", "Dublin"
};

GridView1.DataSource = from city in cities
where city.Length > 4
orderby city
select city.ToUpper();

GridView1.DataBind();
}
</script>

بلافاصله پس از اجرا با خطای زیر روبرو خواهیم شد.



این قطعه کد چون از قابلیت‌های کامپایلر جدید سی شارپ استفاده می‌کند، با کامپایلر پیش فرض و تنظیم شده دات نت 2 کار نخواهد کرد و باید برای رفع این مشکل، فایل web.config جدیدی را نیز به پوشه برنامه اضافه کنیم:

<?xml version="1.0"?>
<configuration>

<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" warningLevel="4" type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<providerOption name="CompilerVersion" value="v3.5"/>
<providerOption name="WarnAsError" value="false"/>
</compiler>
</compilers>
</system.codedom>

<system.web>
<compilation defaultLanguage="c#">
<assemblies>
<add assembly="System.Core, Version=3.5.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
</assemblies>
</compilation>
</system.web>


</configuration>

در اینجا قید اسمبلی System.Core ضروری است و همچنین نگارش کامپایلر نیز به صورت صریح قید شده است تا IIS را وادار کند که از قابلیت‌های جدید دات نت فریم ورک استفاده نماید.

همانطور که ذکر شد اگر از VS.Net 2008 استفاده کنید، هیچ وقت درگیر این مباحث نخواهید شد و همه چیز از پیش تنظیم شده است.