مطالب
SharePoint Client object Model
دو روش اصلی برای دسترسی به داده‌ها از طریق برنامه نویسی در SharePoint وجود دارند. روش اول استفاده از SharePoint API روی سرور است. زمانیکه شما کدی را مستقیم روی سرور SharePoint  اجرا می‌کنید، SharePoint API کنترل کامل تمام جنبه‌های شیرپوینت و داده‌ها را در اختیار شما می‌گذارد. اگر برنامه شما روی سرور اجرا نمی‌شود و نیاز به دسترسی به داده‌های شیرپوینت دارد، لازم است از SharePoint web services استفاده کنید. web services امکاناتی مشابه SharePoint  API را در اختیار شما می‌گذارد؛ هرچند همه امکانات را پوشش نمی‌دهد.

در SharePoint 2010 گزینه دیگری در برنامه نویسی، برای دسترسی به داده‌های SharePoint تدارک دیده شده است: Client Object Model. این یک روش جدید، در برنامه نویسی شیرپوینت است. اگرچه استفاده از web services، پوشش وسیعی از امکانات شیرپوینت را به شما می‌دهد، اما برنامه نویسی به روش Client Object Model و API با استفاده از web services بسیار متفاوت است. استفاده از web services کار را برای شما سخت خواهد کرد و لازم است دو روش برنامه نویسی کاملا مختلف را بیاموزید. همچنین فراخوانی web services با JavaScript پیچیده است و نیازمند ساخت و دستکاری XML‌های فراوان است. Client Object Model تمام این مسائل را حل و برنامه نویسی سمت client را راحت کرده است.

در واقع Client Object Model سه Object Model جدا از هم است:
 نسخه: .NET CLR برای ساخت WinForms, Windows Presentation Foundation (WPF), console applications
 نسخه Silverlight : برای کا با هر دو حالت داخل in-browser و out-of-browser Silverlight applications
 نسخه JavaScript : کدهای Ajax و jQuery را قادر می‌سازد تا داده‌های شیرپوینت را فراخوانی کنند

یکی از سوالاتی که در مورد Client Object Model پیش می‌آید، این است که چه کارهایی را با آن می‌شود انجام داد؟ Client Object Model امکان دسترسی به بیشتر اشیاء رایج را مانند sites, webs, content types, lists, folders, navigations فراهم می‌کند. این اشیا با اسم‌های مشابه در Client Object Model وجود دارند که در جدول زیر مشخص شده‌اند.



 در زیر یک مثال ساده از استفاده‌های Client Object Model را توضیح خواهم داد که لیست‌های موجود در سایت را در خروجی نمایش می‌دهد.
1- در Visual Studio یک پروژه Console application ایجاد کنید.
2- بر روی References کلیک راست کرده Add Reference را انتخاب کنید. از مسیر زیر
 C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI
دو فایل زیر را اضافه کنید
 Microsoft.SharePoint.dll
Microsoft.SharePoint.Client.Runtime.dll

static void Main(string[] args)
        {
            var ctx = new ClientContext(@"http://localhost");
            var web = ctx.Web;
            var lists = web.Lists;
            ctx.Load(lists,
                l => l.Include
                    (list => list.Title).Where
                    (list => list.BaseType == BaseType.GenericList));
            ctx.ExecuteQuery();
            foreach (var list in lists)
                Console.WriteLine(list.Title);
            Console.ReadLine();
}
مطالب
Angular Material 6x - قسمت هشتم - جستجوی کاربران توسط AutoComplete
در مطلب «کنترل نرخ ورود اطلاعات در برنامه‌های Angular» جزئیات پیاده سازی جستجوی همزمان با تایپ کاربر، بررسی شدند. در اینجا می‌خواهیم از اطلاعات آن مطلب جهت پیاده سازی یک  AutoComplete جستجوی نام کاربران که اطلاعات آن از سرور تامین می‌شوند، استفاده کنیم:




استفاده از کامپوننت AutoComplete کتابخانه‌ی Angular Material

کتابخانه‌ی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آن‌را در یک صفحه‌ی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیه‌های آن و کلیک بر روی دکمه‌ی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.


به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعه‌ی کامپوننت‌های تعریف شده اضافه می‌کنیم:
 ng g c contact-manager/components/search-auto-complete --no-spec
همچنین چون قصد داریم آن‌را درون یک popup نمایش دهیم، نیاز است به ماژول contact-manager\contact-manager.module.ts مراجعه کرده و آن‌را به لیست entryComponents نیز اضافه کنیم:
import { SearchAutoCompleteComponent } from "./components/search-auto-complete/search-auto-complete.component";

@NgModule({
  entryComponents: [
    SearchAutoCompleteComponent
  ]
})
export class ContactManagerModule { }

در ادامه برای نمایش این کامپوننت به صورت popup، دکمه‌ی جدید جستجو را به toolbar اضافه می‌کنیم:


برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمه‌ی جستجو را پیش از دکمه‌ی نمایش منو، قرار می‌دهیم:
  <span fxFlex="1 1 auto"></span>
  <button mat-button (click)="openSearchDialog()">
    <mat-icon>search</mat-icon>
  </button>
  <button mat-button [matMenuTriggerFor]="menu">
    <mat-icon>more_vert</mat-icon>
  </button>
با این کدها برای مدیریت متد openSearchDialog در فایل toolbar\toolbar.component.ts
@Component()
export class ToolbarComponent {
  constructor(
    private dialog: MatDialog,
    private router: Router) { }

  openSearchDialog() {
    const dialogRef = this.dialog.open(SearchAutoCompleteComponent, { width: "650px" });
    dialogRef.afterClosed().subscribe((result: User) => {
      console.log("The SearchAutoComplete dialog was closed", result);
      if (result) {
        this.router.navigate(["/contactmanager", result.id]);
      }
    });
  }
}
در اینجا توسط سرویس MatDialog، کامپوننت SearchAutoCompleteComponent به صورت پویا بارگذاری شده و به صورت یک popup نمایش داده می‌شود. سپس مشترک رخ‌داد بسته شدن آن شده و بر اساس اطلاعات کاربری که توسط آن بازگشت داده می‌شود، سبب هدایت صفحه‌ی جاری به صفحه‌ی جزئیات این کاربر یافت شده، خواهیم شد.


کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کننده‌ی از آن

در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده می‌کنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class TypeaheadController : Controller
    {
        private readonly IUsersService _usersService;

        public TypeaheadController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet("[action]")]
        public async Task<IActionResult> SearchUsers(string term)
        {
            return Ok(await _usersService.SearchUsersAsync(term));
        }
    }
}
کدهای کامل متد SearchUsersAsync در مخزن کد این سری موجود هستند.

از این کنترلر به نحو ذیل در برنامه‌ی Angular برای ارسال اطلاعات و انجام جستجو استفاده می‌شود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  searchUsers(term: string): Observable<User[]> {
    return this.http
      .get<User[]>(`/api/Typeahead/SearchUsers?term=${encodeURIComponent(term)}`)
      .pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از اپراتور pipe مخصوص RxJS 6x استفاده شده‌است.


تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete

پس از این مقدمات که شامل تکمیل سرویس‌های سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحه‌ی جستجو به صورت یک popup است، اکنون می‌خواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامه‌های Angular» را پیشتر مطالعه کرده‌اید و با جزئیات آن آشنایی دارید.

تکمیل قالب search-auto-complete.component.html
<h2 mat-dialog-title>Search</h2>
<mat-dialog-content>
  <div fxLayout="column">
    <mat-form-field class="example-full-width">
      <input matInput placeholder="Choose a user" [matAutocomplete]="auto1" 
                (input)="onSearchChange($event.target.value)">
    </mat-form-field>
    <mat-autocomplete #auto1="matAutocomplete" [displayWith]="displayFn" 
                                 (optionSelected)="onOptionSelected($event)">
      <mat-option *ngIf="isLoading" class="is-loading">
        <mat-spinner diameter="50"></mat-spinner>
      </mat-option>
      <ng-container *ngIf="!isLoading">
        <mat-option *ngFor="let user of filteredUsers" [value]="user">
          <span>{{ user.name }}</span>
          <small> | ID: {{user.id}}</small>
        </mat-option>
      </ng-container>
    </mat-autocomplete>
  </div>
</mat-dialog-content>
<mat-dialog-actions>
  <button mat-button color="primary" (click)="showUser()">
    <mat-icon>search</mat-icon> Show User
  </button>
  <button mat-button color="primary" [mat-dialog-close]="true">
    <mat-icon>cancel</mat-icon> Close
  </button>
</mat-dialog-actions>
در این مثال چون کامپوننت search-auto-complete به صورت یک popup ظاهر خواهد شد، ساختار عنوان، محتوا و دکمه‌های دیالوگ در آن پیاده سازی شده‌اند.
سپس نحوه‌ی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده می‌کنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شده‌ی در آن autocomplete اشاره می‌کند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمه‌های فشرده شده‌ی در input box به کامپوننت، از رخداد input استفاده شده‌است. این روش با هر دو نوع حالت مدیریت فرم‌های Angular سازگاری دارد و کدهای آن یکی است.

در کامپوننت mat-autocomplete این تنظیمات صورت گرفته‌اند:
- در لیست ظاهر شده‌ی توسط یک autocomplete، هر نوع ظاهری را می‌توان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده می‌شوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینه‌ای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شده‌است، کمک گرفته خواهد شد.
- از رخ‌داد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده می‌شود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام می‌شود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شده‌ی در کامپوننت استفاده خواهد شد.


تکمیل کامپوننت search-auto-complete.component.ts

کدهای کامل این کامپوننت را در ادامه مشاهده می‌کنید:
import { Component, OnDestroy, OnInit } from "@angular/core";
import { MatAutocompleteSelectedEvent, MatDialogRef } from "@angular/material";
import { Subject, Subscription } from "rxjs";
import { debounceTime, distinctUntilChanged, finalize, switchMap, tap } from "rxjs/operators";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-search-auto-complete",
  templateUrl: "./search-auto-complete.component.html",
  styleUrls: ["./search-auto-complete.component.css"]
})
export class SearchAutoCompleteComponent implements OnInit, OnDestroy {

  private modelChanged: Subject<string> = new Subject<string>();
  private dueTime = 300;
  private modelChangeSubscription: Subscription;
  private selectedUser: User = null;

  filteredUsers: User[] = [];
  isLoading = false;

  constructor(
    private userService: UserService,
    private dialogRef: MatDialogRef<SearchAutoCompleteComponent>) { }

  ngOnInit() {
    this.modelChangeSubscription = this.modelChanged
      .pipe(
        debounceTime(this.dueTime),
        distinctUntilChanged(),
        tap(() => this.isLoading = true),
        switchMap(inputValue =>
          this.userService.searchUsers(inputValue).pipe(
            finalize(() => this.isLoading = false)
          )
        )
      )
      .subscribe(users => {
        this.filteredUsers = users;
      });
  }

  ngOnDestroy() {
    if (this.modelChangeSubscription) {
      this.modelChangeSubscription.unsubscribe();
    }
  }

  onSearchChange(value: string) {
    this.modelChanged.next(value);
  }

  displayFn(user: User) {
    if (user) {
      return user.name;
    }
  }

  onOptionSelected(event: MatAutocompleteSelectedEvent) {
    console.log("Selected user", event.option.value);
    this.selectedUser = event.option.value as User;
  }

  showUser() {
    if (this.selectedUser) {
      this.dialogRef.close(this.selectedUser);
    }
  }
}
- در ابتدای کار کامپوننت، یک modelChanged از نوع Subject اضافه شده‌است. در این حالت با فراخوانی متد next آن در onSearchChange که به رخ‌داد input جعبه‌ی متنی دریافت اطلاعات متصل است، کار انتقال این تغییرات به اشتراک ایجاد شده‌ی به آن در ngOnInit انجام می‌شود. در اینجا بر اساس نکات مطلب «کنترل نرخ ورود اطلاعات در برنامه‌های Angular»، عبارات وارد شده، به سمت سرور ارسال و در نهایت نتیجه‌ی آن به خاصیت عمومی filteredUsers که به حلقه‌ی نمایش اطلاعات mat-autocomplete متصل است، انتساب داده می‌شود. در ابتدای اتصال به سرور، خاصیت isLoading به true و در پایان عملیات به false تنظیم خواهد شد تا mat-spinner را نمایش داده و یا مخفی کند.
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار می‌گیرد، مشخص خواهد شد.
- در متد onOptionSelected، می‌توان به شیء انتخاب شده‌ی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخ‌داد بسته شدن کامپوننت جاری است، ارسال می‌کنیم. به این صورت است که کامپوننت toolbar می‌تواند کار هدایت به جزئیات این کاربر را انجام دهد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
استاندارد وب WIA-ARIA
چند روز پیش مطلبی به عنوان اشتراک در سایت جاری معرفی شده که به ما یادآوری می‌کرد، ما تنها استفاده کنندگان سیستم‌های کامپیوتری، به خصوص اینترنت نیستیم و معلولین هم نیازمند استفاده از این فناوری‌ها هستند.
WAI-ARIA  که برگرفته از Web Accessibility Initiative - Accessible Rich internet Application است به معنی برنامه‌ی اینترنتی تعامل گرا با خاصیت دسترسی پذیری بالا می‌باشد و یک راهنماست که توسط کنسرسیوم وب (+ ) معرفی گشته است تا وب سایت‌ها با رعایت این قوانین، دسترسی سایت خود را بالاتر ببرند. این قوانین به خصوص برای سایت‌هایی با محتوای پویا هستند که از فناوری‌هایی چون Ajax,Javascrip, HTML و دیگر فناوری‌های مرتبط استفاده می‌کنند.
امروزه طراحان وب بیش از هر وقتی از فناوری‌های سمت کلاینت چون جاوااسکریپت برای ساخت رابط‌های کاربری استفاده می‌کنند که html به تنها قادر به ایجاد آن‌ها نیست. یکی از تکنیک‌های جاوااسکریپت، دریافت محتوای جدید و به روزآوری قسمتی از صفحات وب است بدون اینکه مجددا کل صفحه از وب سرور درخواست گردد که به این تکنیک  Rich Internet Application هم می‌گویند. تا به اینجای کار هیچ مشکلی نیست و خوب هم هست؛ ولی مشکلی که در این بین وجود دارد این است که این نوع تکنیک‌ها باعث از بین رفتن خاصیت دسترسی پذیری معلولین می‌گردند. معلولینی که از صفحه خوان ها استفاده می‌کنند یا به دلیل معلولیت‌های خود قادر به حرکت دادن ماوس نیستند.
ARIA با استفاده از خصوصیت‌ها Properties، نقش‌ها Roles و وضعیت‌ها States به طراحان برنامه‌های وب و سازندگان فناوری‌های یاری رسان، اجازه می‌دهد که با ابزارهای کمکی معلولان ارتباط برقرار کنیم و یک صفحه‌ی وب ساده را به یک صفحه‌ی پویا تبدیل کنیم. ARIA تنها یک استاندارد برای وب نیست، بلکه یک فناوری چند پلتفرمه است که برای بازی‌های رایانه‌ای، موبایل‌ها، دستگاه‌های سرگرمی و سلامتی و دیگر انواع برنامه‌ها نیز تعریف شده است.



فریمورک ARIA
خود HTML به تنهایی نمی‌تواند نقش‌های هر المان و ارتباط بین آن‌ها را به درستی بیان کند و به این منظور ARIA به کمک می‌آید. با استفاده از نقش‌ها میتوان هدف هر المان را مشخص کرد و با استفاده از خصوصیات ARIA نحوه‌ی عملکرد آن‌ها را تعریف کرد.

نقش ها
نقش‌ها طبق مستندات کنسرسیوم وب بر 4 نوع هستند:
  • Abstract Roles
  • Widget Roles
  • Document Roles
  • Landmark Roles
فریمورک ARIA با استفاده از Landmark Roles یا نقش‌های راهنما، به معرفی بخش‌های ویژوالی می‌پردازد تا فناوری‌های کمکی به آن دسترسی سریعی داشته باشند. هشت نقش راهنما وجود دارد که در زیر آن‌ها را بررسی می‌کنیم.

 Banner این قسمت که عموما برای اجزای مهمی مثل هدر سایت قرار می‌گیرد و شامل معرفی وب سایت هست و در همه‌ی صفحات وجود دارد که شامل لوگو، اطلاعات عمومی سایت و اسپانسرها و ... می‌گردد و بسیار مهم است که تنها یکبار در صفحه‌ی وب به کار برود و تکرار آن پرهیز شود.
 Main  این نقش به محتوای اصلی وب سایت اشاره می‌کند و نباید بیشتر از یکبار در هر صفحه‌ی وب به کار برود و عموما بهتر است این خصوصیت در تگ div قرار گیرد:
<div Role="main"></div>
یا در HTML5 به طور مفهومی ساخته می‌شود:
<main role="main">.....

Navigation اشاره به یک ناحیه پر از المان‌های لینک برای ارتباط با صفحات دیگر
 Complementary  مشخص سازی ناحیه‌ای که اطلاعات اضافی درباره‌ی محتوای اصلی سایت دارد؛ مانند بخش مقالات مرتبط، آخرین کامنت‌ها و ...
 ContentInfo  این نقش که بیشتر برای فوتر مناسب است برای محتوایی به کار می‌رود که در آن به قوانین کپی رایت و ... اشاره می‌شود.
 form  برای اشاره به فرم‌ها که دارای قسمت‌های ورودی کاربر هستند.
 search
 در صورتیکه فرمی دارید و از آن برای گزینه‌ی جست و جو استفاده می‌کنید، از این نقش استفاده کنید.
 application  برای اینکه وب سایت خود را به صورت یک وب اپلیکیشن معرفی کنید؛ تا یک صفحه وب معمولی، استفاده می‌شود و برای وب سایت‌های قدیمی یا با حالت سنتی توصیه نمی‌شود و به برنامه‌های کمکیار معلولین می‌گوند که از حالت عادی به حالت application سوئیچ کنند؛ پس با دقت بیشتری باید از این گزینه استفاده کرد.
 


خصوصیت‌ها و وضعیت ها

در حالیکه از نقش‌ها برای معرفی هر المان یا تگ استفاده می‌کنید؛ خصویت‌ها و وضعیت‌ها به کاربر اطلاعات اضافی می‌دهند که چگونه ز آن استفاده کنند. برای معرفی خصوصیت‌ها و وضعیت‌ها از یک خصوصیت که با -aria شروع می‌شود استفاده کنید. از معروفترین آن‌ها خصوصیت aria-required و وضعیت aria-checked می‌باشند، تا به ترتیب به کاربر اعلام کنید این تگ نیاز به پر شدن دارد، یا المانی نیاز به تغییر وضعیت انتخابی دارد.
نحوه‌ی استفاده از آن‌ها به شکل زیر است:
<div id="some-id" class="some-class" aria-live="assertive"><div>
aria-live سه مقدار می‌پذیرد که عبارتند از Off,Polite,Assertive  و مشخص می‌کنند که المان مورد نظر آپدیت پذیر هست یا خیر و اگر آری، نحوه‌ی به روز شوندگی آن به چه نحوی است.

ساخت ارتباطات میان المان‌ها با خصوصیت‌های ارتباطی

مثال شماره یک
<div role="main" aria-labelledby="some-id">
 
    <h1 id="some-id">This Is A Heading</h1>
 
    Main content...
 
</div>
خصوصیت aria-labelledby به تعریف المان‌هایی با نام جاری می‌پردازد. این خصوصیت معرفی کننده‌ی برچسب هاست. به عنوان مثال بین المان‌ها ورودی و برچسب آنان ارتباط ایجاد می‌کند تا ابزارهای معلولین، مانند صفحه خوان‌ها، در خواندن به مشکل برنخورند.

مثال شماره دو
در این مثال هر گروهی از المان‌ها یک برچسب و بعضی المان‌ها یک برچسب اختصاصی دارند که توسط خصوصیت معرفی شده aria-labelldby کامل شده‌اند:
<div id="billing">Billing Address</div>

<div>
    <div id="name">Name</div>
    <input type="text" aria-labelledby="name billing"/>
</div>
<div>
    <div id="address">Address</div>
    <input type="text" aria-labelledby="address billing"/>
</div>

مثال شماره سه

در این مثال گروهی از radio button‌ها با برچسبشان ارتباط برقرار می‌کنند.
<div id="radio_label">My radio label</div>
<ul role="radiogroup" aria-labelledby="radio_label">
    <li role="radio">Item #1</li>
    <li role="radio">Item #2</li>
    <li role="radio">Item #3</li>
</ul>

خصوصیت‌ها و وضعیت‌های aria را با چرخه‌ی فعالیت‌های صفحه به روز کنید

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


هر aria را دوباره استفاده نکنید

امروزه به خصوص با آمدن html5 و ویژگیهایی چون تگ‌های مفهومی، کار بسیار راحت‌تر شده‌است و مرورگر به طور خودکار می‌تواند aria را بر روی بعضی از المان‌ها پیاده کند. به عنوان نمونه:
<form></form>
<form role="form"></form>
همچنین به عنوان مثال با استفاده از خصوصیات HTML مثل hidden کردن یک شیء نیازی به استفاده از وضعیت aria-hidden نمی‌باشد. مرورگر به طور پیش فرض آن را لحاظ می‌کند.

امروزه شاهد پیشرفت فناوری در همه‌ی عرصه‌ها هستیم و همیشه این پیشرفت‌ها ما را ذوق زده کرده‌اند، ولی یکی از بی نظیرترین استفاده‌های فناوری روز، استفاده در صنایع سلامتی است که نه تنها ما را ذوق زده می‌کند، بلکه از لحاظ احساسی هم ما را به وجد می‌آورند و جزء زیباترین نتایج فناوری می‌باشند. بسیاری از شرکت‌ها چون گوگل در این راستا فعالیت‌های زیادی کرده‌اند تا بتوانند سلامت جامعه را کنترل کنند، از ساخت لنز چشمی برای کنترل دیابت گرفته تا ساخت قاشق عذای خوری برای بیماران پارکینسون، ولی استفاده‌های ساده از مسائلی مانند بالا به افراد معلول این مژده را می‌دهد که ما آن‌ها را فراموش نکرده‌ایم.
نظرات مطالب
چک لیست تهیه یک هاست خوب برای تازه کاران
همه این موراد به خصوص توان سرویس دهی نیاز به تحقیق داره
چون توان سرویس دهی چیزی نیست که بتوان یا دستوری یا تستی متوجه آن شد. چون باید مدتی سرویس داشته باشید تا متوجه بشوید اوضاع سرویس دهی آنان چگونه است. مثلا در هفته در ماه چقدر وب سایت شما از دسترس خارج می‌شود.
با استفاده از پینگ زدن سرعت هاست را چک نمایید. لینک‌های زیر را هم ببینید:
WebsiteSpeedTest
Http/Https Server Test
DNS Hosting Test
مطالب
تبدیل پلاگین‌های jQuery‌ به کنترل‌های ASP.Net

امروز داشتم یک سری از پلاگین‌های jQuery را مرور می‌کردم، مورد زیر به نظرم واقعا حرفه‌ای اومد و کمبود آن هم در بین کنترل‌های استاندارد ASP.Net محسوس است:
Masked Input Plugin
استفاده از آن به صورت معمولی بسیار ساده است. فقط کافی است اسکریپت‌های jQuery و سپس این افزونه به هدر صفحه اضافه شوند و بعد هم مطابق صفحه usage آن عمل کرد.
خیلی هم عالی! ولی این شیوه‌ی متداول کار در ASP.Net نیست. آیا بهتر نیست این مجموعه را تبدیل به یک کنترل کنیم و از این پس به سادگی با استفاده از Toolbox ویژوال استودیو آن‌را به صفحات اضافه کرده و بدون درگیر شدن با دستکاری سورس html صفحه، از آن استفاده کنیم؟ به‌عبارتی دیگر یکبار باید با جزئیات درگیر شد، آنرا بسته بندی کرد و سپس بارها از آن استفاده نمود. (مفاهیم شیءگرایی)

برای این‌کار، یک پروژه جدید ایجاد ASP.Net server control را آغاز نمائید (به نام MaskedEditCtrl).



به صورت پیش فرض یک قالب استاندارد ایجاد خواهد شد که کمی نیاز به اصلاح دارد. نام کلاس را به MaskedEdit تغییر خواهیم داد و همچنین در قسمت ToolboxData نیز نام کنترل را به MaskedEdit ویرایش می‌کنیم.
برای اینکه مجبور نشویم یک کنترل کاملا جدید را از صفر ایجاد کنیم، خواص و توانایی‌های اصلی این کنترل را از TextBox استاندارد به ارث خواهیم برد. بنابراین تا اینجای کار داریم:
namespace MaskedEditCtrl
{
[DefaultProperty("MaskFormula")]
[ToolboxData("<{0}:MaskedEdit runat=server></{0}:MaskedEdit>")]
[Description("MaskedEdit Control")]
public class MaskedEdit : TextBox

{

سپس باید رویداد OnPreRender را تحریف (override) کرده و در آن همان اعمالی را که هنگام افزودن اسکریپت‌ها به صورت دستی انجام می‌دادیم با برنامه نویسی پیاده سازی کنیم. چند نکته ریز در اینجا وجود دارد که در ادامه به آن‌ها اشاره خواهد شد.
از ASP.Net 2.0 به بعد، امکان قرار دادن فایل‌های اسکریپت و یا تصاویر همراه یک کنترل، درون فایل dll آن بدون نیاز به توزیع مجزای آنها به صورت WebResource مهیا شده است. برای این منظور اسکریپت‌های jQuery و افزونه mask edit را به پروژه اضافه نمائید. سپس به قسمت خواص آنها (هر دو اسکریپت) مراجعه کرده و build action آنها را به Embedded Resource تغییر دهید (شکل زیر):



از این پس با کامپایل پروژه، این فایل‌ها به صورت resource به dll ما اضافه خواهند شد. برای تست این مورد می‌توان به برنامه reflector مراجعه کرد (تصویر زیر):



پس از افزودن مقدماتی اسکریپت‌ها و تعریف آنها به صورت resource ، باید آنها را در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: WebResource("MaskedEditCtrl.jquery.min.js", "text/javascript")]
[assembly: WebResource("MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js", "text/javascript")]

نکته مهم: همانطور که ملاحظه می‌کنید نام فضای نام پروژه (namespace) باید به ابتدای اسکریپت‌های معرفی شده اضافه شود.

پس از آن نوبت به افزودن این اسکریپت‌ها به صورت خودکار در هنگام نمایش کنترل است. برای این منظور داریم:
        protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);

//adding .js files
if (!Page.ClientScript.IsClientScriptIncludeRegistered("jquery_base"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.min.js");
Page.ClientScript.RegisterClientScriptInclude("jquery_base", scriptUrl);
}

if (!Page.ClientScript.IsClientScriptIncludeRegistered("edit_ctrl"))
{
string scriptUrl = Page.ClientScript.GetWebResourceUrl(this.GetType(),
"MaskedEditCtrl.jquery.maskedinput-1.1.4.pack.js");
Page.ClientScript.RegisterClientScriptInclude("edit_ctrl", scriptUrl);
}

if (!Page.ClientScript.IsStartupScriptRegistered("MaskStartup" + this.ID))
{
// Form the script to be registered at client side.
StringBuilder sbStartupScript = new StringBuilder();
sbStartupScript.AppendLine("jQuery(function($){");
sbStartupScript.AppendLine("$(\"#" + this.ClientID + "\").mask(\"" + MaskFormula + "\");");
sbStartupScript.AppendLine("});");
Page.ClientScript.RegisterStartupScript(typeof(Page),
"MaskStartup" + this.ID, sbStartupScript.ToString(), true);
}

}

همانطور که ملاحظه می‌کنید، ابتدا WebResource دریافت شده و سپس به صفحه اضافه می‌شود. در آخر مطابق راهنمای افزونه mask edit عمل شد. یعنی اسکریپت مورد نظر را ساخته و به صفحه اضافه کردیم.

نکته جاوا اسکریپتی: علت استفاده از this.ClientID جهت معرفی نام کنترل جاری این است که هنگامیکه کنترل توسط یک master page رندر شود، ID آن توسط موتور ASP.Net کمی تغییر خواهد کرد. برای مثال myTextBox‌ به ctl00_ContentPlaceHolder1_myTextBox تبدیل خواهد شد و اگر صرفا this.ID ذکر شده باشد دیگر دسترسی به آن توسط کدهای جاوا اسکریپت مقدور نخواهد بود. بنابراین از ClientID جهت دریافت ID نهایی رندر شده توسط ASP.Net کمک می‌گیریم.

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

[Description("MaskedEdit Formula such as 99/99/9999")]
[Bindable(true), Category("MaskedEdit"), DefaultValue(0)]
public string MaskFormula
{
get
{
if (ViewState["MaskFormula"] == null) ViewState["MaskFormula"] = "99/99/9999";
return (string)ViewState["MaskFormula"];
}
set { ViewState["MaskFormula"] = value; }

}

این خاصیت public هنگام نمایش در Visual studio به شکل زیر درخواهد آمد:



نکته مهم: در اینجا حتما باید از view state جهت نگهداری مقدار این خاصیت استفاده کرد تا در حین post back ها مقادیر انتساب داده شده حفظ شوند.

اکنون پروژه را کامپایل کنید. برای افزودن کنترل ایجاد شده به toolbox می‌توان مطابق تصویر عمل کرد:



نکته: برای افزودن آیکون به کنترل (جهت نمایش در نوار ابزار) باید: الف) تصویر مورد نظر از نوع bmp باشد با اندازه 16 در16 pixel . ب) باید آنرا به پروژه افزود و build action آن را به Embedded Resource تغییر داد. سپس آنرا در فایل AssemblyInfo.cs پروژه نیز تعریف کرد (به صورت زیر).

[assembly: System.Web.UI.WebResource("MaskedEditCtrl.MaskedEdit.bmp", "img/bmp")]

کنترل ما پس از افزوده شدن، شکل زیر را خواهد داشت:


جهت دریافت سورس کامل و فایل بایناری این کنترل، اینجا کلیک کنید.


مطالب
نمایش ساختارهای درختی توسط jqGrid
jqGrid از نمایش دو ساختار درختی Nested Set model و Adjacency model پشتیبانی می‌کند. توضیحات تکمیلی و پایه‌ای را در مورد این دو روش مدل سازی اطلاعات، در مطلب «SQL Antipattern #2» می‌توانید مطالعه کنید.
در اینجا روش Adjacency model را به علت بیشتر مرسوم بودن آن و شباهت بسیار زیاد آن به «مدل‌های خود ارجاع دهنده» بررسی خواهیم کرد.


مدل داده‌ای Adjacency

در حالت ساختار درختی از نوع مجاورت، علاوه بر خواص اصلی یک کلاس، سه خاصیت دیگر نیز باید تعریف شوند:
using System;

namespace jqGrid13.Models
{
    public class BlogComment
    {
        // Other properties 
        public int Id { set; get; }
        public string Body { set; get; }
        public DateTime AddDateTime { set; get; }

        // for treeGridModel: 'adjacency'
        public int? ParentId { get; set; }
        public bool IsNotExpandable { get; set; }
        public bool IsExpanded { get; set; }
    }
}
ParentId که سبب تولید یک مدل خود ارجاع دهنده می‌شود.
IsNotExpandable به این معنا است که نود جاری آیا قرار است باز شود و فرزندی دارد یا خیر؟ اگر فرزندی ندارد باید مساوی True قرار گیرد.
IsExpanded حالت پیش فرض باز بودن یا نبودن یک نود را مشخص می‌کند.


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

در نگارش فعلی jqGrid، در حالت نمایش درختی، مباحث صفحه بندی و مرتب سازی غیرفعال هستند و کدهای مرتبط با آن که در اینجا ذکر شده‌اند، فعلا تاثیری ندارند (البته با کمی تغییر در کدهای آن، می‌توان این قابلیت را هم فعال کرد. اطلاعات بیشتر).
نکته‌ی مهم treeGrid، سه پارامتر دیگر هستند که از سمت کلاینت به سرور ارسال می‌شوند:
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using jqGrid13.Models;
using JqGridHelper.DynamicSearch; // for dynamic OrderBy
using JqGridHelper.Models;
using JqGridHelper.Utils;

namespace jqGrid13.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult GetComments(JqGridRequest request, int? nodeid, int? parentid, int? n_level)
        {
            var list = BlogCommentsDataSource.LatestBlogComments;

            // در این حالت خاص فعلا در نگارش جای جی‌کیو‌گرید صفحه بندی کار نمی‌کند و فعال نیست و محاسبات ذیل اهمیتی ندارند
            var pageIndex = request.page - 1;
            var pageSize = request.rows;
            var totalRecords = list.Count;
            var totalPages = (int)Math.Ceiling(totalRecords / (float)pageSize);

            var productsQuery = list.AsQueryable();
            
            if (nodeid == null)
            {
                productsQuery = productsQuery.Where(x => x.ParentId == null);
            }
            else
            {
                productsQuery = productsQuery.Where(x => x.ParentId == nodeid.Value);
            }

            var products = productsQuery.OrderBy(request.sidx + " " + request.sord)
                                        .Skip(pageIndex * pageSize)
                                        .Take(pageSize)
                                        .ToList();


            var newLevel = n_level == null ? 0 : n_level.Value + 1;
            var productsData = new JqGridData
            {
                Total = totalPages,
                Page = request.page,
                Records = totalRecords,
                Rows = (products.Select(comment => new JqGridRowData
                {
                    Id = comment.Id,
                    RowCells = new List<object> 
                               {
                                   comment.Id,
                                   comment.Body,
                                   comment.AddDateTime.ToPersianDate(),
                                   // اطلاعات خاص نمایش درختی به ترتیب
           newLevel,
           comment.ParentId == null ? "" : comment.ParentId.Value.ToString(CultureInfo.InvariantCulture),
           comment.IsNotExpandable,
           comment.IsExpanded
                               }
                })).ToList()
            };
            return Json(productsData, JsonRequestBehavior.AllowGet);
        }
    }
}
nodeid اگر نال بود، یعنی کل اطلاعات ریشه‌ها (مواردی که parentId مساوی نال دارند)، باید واکشی شوند. اگر nodeid مقدار داشت، یعنی فرزند نود جاری قرار است بازگشت داده شود.
n_level مقدار جلو رفتگی نمایش اطلاعات یک نود را مشخص می‌کند. در اینجا چون با کلیک بر روی هر نود، فرزند آن از سرور واکشی می‌شود و lazy loading برقرار است، بازگشت مقدار n_level دریافتی از کلاینت به علاوه یک، کافی است. اگر نیاز است تمام نودها باز شده نمایش داده شوند، این مورد را باید به صورت دستی محاسبه کرده و در مدل BlogComment پیش بینی کنید.
در نهایت آرایه‌ای از خواص مدنظر به همراه 4 خاصیت ساختار درختی باید به ترتیب بازگشت داده شوند.




فعال سازی سمت کاربر treeGrid

برای فعال سازی سمت کاربر نمایش درختی اطلاعات، باید سه خاصیت ذیل تنظیم شوند:
            $('#list').jqGrid({
                caption: "آزمایش سیزدهم",
                // .... مانند قبل
                treeGrid: true,
                treeGridModel: 'adjacency',
                ExpandColumn: '@(StronglyTyped.PropertyName<BlogComment>(x => x.Body))'
            }).jqGrid('gridResize', { minWidth: 400 });
        });
تنظیم treeGrid: true سبب فعال سازی treeGrid می‌شود. توسط treeGridModel حالات Nested Set model و Adjacency model قابل تنظیم هستند و ExpandColumn نام ستونی را مشخص می‌کند که قرار است فرزندان آن نمایش داده شوند.


یک نکته‌ی تکمیلی

اگر می‌خواهید دقیقا به شکل زیر برسید:

تنظیم rownumbers: true گرید را حذف کنید. همچنین ستون Id را نیز با تنظیم‌های hidden:true, key: true مخفی نمائید (در تعاریف colModel).


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid13.zip


برای مطالعه بیشتر
Tree Grid
Nested Set Model
Adjacency Model
مطالب
سریع‌تر کردن عملیات دریافت‌ اطلاعات از وب، توسط یک برنامه ASP.Net

فرض کنید یک برنامه ASP.Net نوشته‌اید که کار آن نمایش یک سری فید از سایت‌های مختلف است یا دریافت وضعیت آب و هوا از یک وب سرویس و نمایش آن در سایت می‌باشد و امثال آن (استفاده از WebRequest ، WebClient ، XmlDom/Reader و ...).
اگر همین عملیات را یکبار با ASP.Net 1.1 و بار دیگر با ASP.Net 2.0 به بالا انجام دهید، متوجه تفاوت سرعت دریافت قابل تاملی خواهید شد (ASP.Net 2.0 به بعد حدودا تا 10 ثانیه کندتر عمل می‌کند). علت چیست؟
در دات نت 1.1 ، تنظیمات پروکسی پیش فرض در کتابخانه‌های مربوطه وجود نداشت و به نال تنظیم شده بود. در دات نت 2 به بعد این مورد به پروکسی پیش فرض سیستم، تنظیم شده است. پروکسی پیش فرض سیستم همان تنظیماتی است که در internet explorer صورت می‌گیرد.
کاربر پیش فرض ASP.Net (مثلا NETWORK SERVICE) دسترسی خواندن این اطلاعات را از رجیستری ویندوز ندارد. علت این وقفه هم همین مورد است! (حتی اگر برنامه ویندوزی شما هم دسترسی خواندن اطلاعات کلیدهای HKLM رجیستری ویندوز را نداشته باشد، باز هم این مساله رخ خواهد داد)
دات نت فریم ورک سعی می‌کند تا این تنظیمات را از رجیستری یا مکان‌های میسر دیگر بخواند و در آخر پس از شکست کلیه حالات مختلف، کلاینت را به صورت مستقیم متصل خواهد کرد.
خوشبختانه این عملکرد پیش فرض قابل تغییر است. تنها کافی است چند سطر زیر را به فایل config برنامه خود اضافه کنید:

<system.net>
<defaultProxy>
<proxy bypassonlocal="true" usesystemdefault="false" />
</defaultProxy>
</system.net>

با این‌کار تشخیص خودکار پروکسی سیستم غیرفعال شده و وقفه‌ی معرفی شده دیگر وجود نخواهد داشت.
راه دیگر انجام آن، نسبت دادن نال به خاصیت Proxy شیء HttpWebRequest است که همین اثر را خواهد داشت.

مطالب
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
همانطور که در مطلب «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» نیز ذکر شد، خاصیت editrules یک ستون، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool هستند، minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است. در ادامه نحوه‌ی اعمال اعتبارسنجی‌های سفارشی سمت سرور و همچنین سمت کلاینت را بررسی خواهیم کرد.


مدل برنامه و نیازمندی‌های اعتبارسنجی آن

namespace jqGrid08.Models
{
    public class User
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Email { set; get; }

        public string Password { set; get; }

        public string SiteUrl { set; get; }
    }
}
مدل کاربر فوق را در نظر بگیرید. در حین ورود اطلاعات نیاز است:
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شده‌ی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمه‌ی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمه‌ی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.


اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        align: 'right', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true,
                            custom: true,
                            custom_func: function (value, colname) {
                                if (!value)
                                    return [false, "لطفا نامی را وارد کنید"];

                                if (value.length < 3 || value.length > 40)
                                    return [false, colname + " باید بین 3 تا 40 حرف باشد"];
                                else
                                    return [true, ""];
                            }
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری نام کاربر به پایان می‌رسد. اما نیاز است این نام بین 3 تا 40 حرف باشد. بنابراین نیاز است سمت کاربر بتوان اطلاعات وارد شده توسط کاربر را دریافت کرده و سپس طول آن‌را بررسی نمود. این‌کار، توسط مقدار دهی خاصیت custom به true و سپس تعریف متدی برای custom_func قابل انجام است.
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بوده‌است؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان می‌رسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالت‌های Add و Edit را بررسی کنیم:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    afterSubmit: showServerSideErrors
                },
                //add options
                {
                    afterSubmit: showServerSideErrors
                },
                //delete options
                {
                });
        });

        function showServerSideErrors(response, postdata) {
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                //و همچنین جلوگیری از ثبت نهایی فرم
                return [false, result.message, result.id];
            }

            return [true, "", result.id];
        }
شیء response، حاوی اطلاعات بازگشت داده شده از طرف سرور است. برای مثال یک چنین خروجی JSON ایی را در حالت‌های شکست اعتبارسنجی بازگشت می‌دهیم:
        [HttpPost]
        public ActionResult AddUser(User postData)
        {
            //todo: Add user to repository
            if (postData == null)
                return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet);

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            if (_usersInMemoryDataSource.Any(
                    user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1;
            _usersInMemoryDataSource.Add(postData);

            return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet);
        }
در سمت کلاینت در روال رویدادگردان afterSubmit می‌توان با آنالیز response و سپس استخراج فیلدهای JSON آن، وضعیت success و همچنین پیام‌های بازگشت داده شده را بررسی کرد.
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.



اعتبار سنجی ایمیل کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 250,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            email: true
                        },
                        formatter: 'email'
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری ایمیل کاربر به پایان می‌رسد. همچنین با تنظیم email: true، به صورت خودکار فرمت ایمیل وارد شده نیز بررسی می‌شود.
مطابق نیازمندی‌های اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت می‌شود.



اعتبار سنجی کلمه عبور کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        align: 'center', width: 70,
                        editable: true, edittype: 'password',
                        editoptions: {
                            maxlength: 10,
                            dir: 'ltr'
                        },
                        editrules: {
                            //required: true ---> در این حالت خاص قابل استفاده نیست
                            //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                            //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        }
                    },
                ],
حالت بررسی اعتبارسنجی کلمه‌ی عبور در اینجا، حالت ویژه‌ای است. نیاز است در حالت ثبت اطلاعات اجباری باشد اما در حالت ویرایش خیر. بنابراین نمی‌توان از خاصیت required: true استفاده کرد؛ چون به هر دو حالت ویرایش و ثبت اطلاعات به صورت یکسان اعمال می‌شود.
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    /*, beforeSubmit: function (posdata, obj) {
                        //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                        return [true, ""];
                    }*/
                },
                //add options
                {
                    beforeSubmit: function (postdata, obj) {
                        //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined)
                            return [false, "لطفا کلمه عبور را وارد کنید"];
                        return [true, ""];
                    }
                },
                //delete options
                {
                });
        });
چون می‌خواهیم تنها حالت Add را تحت کنترل قرار دهیم، رویدادگردان beforeSubmit آن‌را مقدار دهی کرده‌ایم. توسط postdata کلیه اطلاعات قابل ارسال به سرور به صورت یک شیء جاوا اسکریپتی یا JSON در اختیار ما است. سپس با بررسی برای مثال postdata.Password می‌توان در مورد مقدار کلمه‌ی عبور تصمیم گیری کرد. در اینجا نیز خروجی متد باید یک آرایه دو عضوی باشد تا در صورت false بودن اولین عضو آن، پیام سفارشی اعتبارسنجی خاصی را بتوان به کاربر نمایش داد.



اعتبار سنجی آدرس سایت کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 1000,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            url: true
                        },
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='" + cellvalue + "' >" + cellvalue + "</a>";
                        },
                        unformat: function (cellvalue, options, cell) {
                            return $('a', cell).attr('href');
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری آدرس سایت کاربر به پایان می‌رسد. همچنین با تنظیم url: true، به صورت خودکار فرمت URL وارد شده نیز بررسی می‌شود.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid08.zip
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت اول
پس از آشنایی مقدماتی با اجزای مهم تشکیل دهنده‌ی Ember.js (^ و ^)، بهتر است این دانسته‌ها را جهت تکمیل یک پروژه‌ی ساده‌ی تک صفحه‌ای بلاگ، بکار بگیریم.
در این بلاگ می‌توان:
- یک مطلب جدید را ارسال کرد.
- مطالب قابل ویرایش و یا حذف هستند.
- مطالب بلاگ قسمت ارسال نظرات دارند.
- امکان گزارشگیری از آخرین نظرات ارسالی وجود دارد.
- سایت صفحات درباره و تماس با ما را نیز دارا است.


ساختار پوشه‌های برنامه

در تصویر ذیل، ساختار پوشه‌های برنامه بلاگ را ملاحظه می‌کنید. چون قسمت سمت کلاینت این برنامه کاملا جاوا اسکریپتی است، پوشه‌های App، Controllers، Libs، Models، Routes و Templates آن در پوشه‌ی Scripts تعریف شده‌اند و به این ترتیب می‌توان تفکیک بهتری را بین اجزای تشکیل دهنده‌ی یک برنامه‌ی تک صفحه‌ای وب Emeber.js پدید آورد.


فایل CSS بوت استرپ نیز به پوشه‌ی Content اضافه شده‌است.


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

در ساختار پوشه‌های فوق، از پوشه‌ی Libs برای قرار دادن کتابخانه‌های پایه برنامه مانند jQuery و Ember.js استفاده خواهیم کرد. به این ترتیب:
- نیاز به آخرین نگارش‌های Ember.js و همچنین افزونه‌ی Ember-Data آن برای کار ساده‌تر با داده‌ها و سرور وجود دارد. این فایل‌ها را از آدرس ذیل می‌توانید دریافت کنید (نسخه‌‌های نیوگت به دلیل قدیمی بودن و به روز نشدن مداوم آن‌ها توصیه نمی‌شوند):
http://emberjs.com/builds/#/beta
برای حالت آزمایش برنامه، استفاده از فایل‌های دیباگ آن توصیه می‌شوند (فایل‌هایی با نام اصلی و بدون پسوند prod یا min). زیرا این فایل‌ها خطاها و اطلاعات بسیار مفصلی را از اشکالات رخ داده، در کنسول وب مرورگرها، فایرباگ و یا Developer tools آن‌ها نمایش می‌دهند. نسخه‌ی min برای حالت ارائه‌ی نهایی برنامه است. نسخه‌ی prod همان نسخه‌ی دیباگ است با حذف اطلاعات دیباگ (نسخه‌ی production فشرده نشده). نسخه‌ی فشرده شده‌ی prod آن، فایل min نهایی را تشکیل می‌دهد.
- دریافت جی‌کوئری
- آخرین نگارش handlebars.js را از سایت رسمی آن دریافت کنید: http://handlebarsjs.com
- Ember Handlebars Loader: https://github.com/michaelrkn/ember-handlebars-loader
- Ember Data Local Storage Adapter: https://github.com/kurko/ember-localstorage-adapter

ترتیب تعریف و قرارگیری این فایل‌ها را پس از دریافت، در فایل index.html قرار گرفته در ریشه‌ی سایت، در کدهای ذیل مشاهده می‌کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Ember Blog</title>
 
  <link href="Content/bootstrap.css" rel="stylesheet" />
  <link href="Content/bootstrap-theme.css" rel="stylesheet" />
  <link href="Content/styles.css" rel="stylesheet" />
 
  <script src="Scripts/Libs/jquery-2.1.1.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/bootstrap.min.js" type="text/javascript"></script>
  <script src="Scripts/Libs/handlebars-v2.0.0.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-handlebars-loader-0.0.1.js" type="text/javascript"></script>
  <script src="Scripts/Libs/ember-data.js" type="text/javascript"></script>
  <script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script> 
</head>
<body>
 
</body>
</html>


اصلاح فایل ember-handlebars-loader-0.0.1.js
اگر به فایل ember-handlebars-loader-0.0.1.js مراجعه کنید، مسیر فایل‌های قالب handlebars قسمت‌های مختلف برنامه را از پوشه‌ی templates واقع در ریشه‌ی سایت می‌خواند. با توجه به تصویر ساختار پوشه‌ی پروژه‌ی جاری، پوشه‌ی template به داخل پوشه‌ی Scripts منتقل شده‌است و نیاز به یک چنین اصلاحی دارد:
 url: "Scripts/Templates/" + name + ".hbs",
کار اسکریپت ember-handlebars-loader-0.0.1.js بارگذاری خودکار فایل‌های hbs یا handlebars از پوشه‌ی قالب‌های سفارشی برنامه است. در این حالت اگر برنامه را اجرا کنید، خطای 404 را دریافت خواهید کرد. از این جهت که mime-type پسوند hbs در IIS تعریف نشده‌است. اضافه کردن آن نیز ساده‌است. به فایل web.config برنامه مراجعه کرده و تغییر ذیل را اعمال کنید:
 <system.webServer>
  <staticContent>
        <mimeMap fileExtension=".hbs" mimeType="text/x-handlebars-template" />
  </staticContent>
 </system.webServer>


مزیت استفاده از نسخه‌ی دیباگ ember.js در حین توسعه‌ی برنامه

نسخه‌ی دیباگ ember.js علاوه بر به همراه داشتن خطاهای بسیار جامع و توضیح علل مشکلات، مواردی را که در آینده منسوخ خواهند شد نیز توضیح می‌دهد:


برای مثال در اینجا عنوان شده‌است که دیگر از linkTo استفاده نکنید و آن‌را به link-to تغییر دهید.
همچنین اگر از مرورگر کروم استفاده می‌کنید، افزونه‌ی Ember Inspector را نیز می‌توانید نصب کنید تا اطلاعات بیشتری را از جزئیات مسیریابی‌های تعریف شده و قالب‌های Ember.js بتوان مشاهده کرد. این افزونه به صورت یک برگه‌ی جدید در Developer tools آن ظاهر می‌شود.


ایجاد شیء Application

همانطور که در قسمت‌های پیشین نیز عنوان شد (^ و ^  )، یک برنامه‌ی Ember.js با تعریف وهله‌ای از شیء Application آن آغاز می‌شود. برای این منظور به پوشه‌ی App مراجعه کرده و فایل جدید Scripts\App\blogger.js را اضافه کنید؛ با این محتوا:
 Blogger = Ember.Application.create();
مدخل این فایل را نیز پس از تعاریف وابستگی‌های اصلی برنامه، به فایل index.html اضافه خواهیم کرد:
<script src="Scripts/App/blogger.js" type="text/javascript"></script>
تا اینجا برپایی اولیه‌ی برنامه‌ی تک صفحه‌ای وب مبتنی بر Ember.js ما به پایان می‌رسد.


تعاریف مسیریابی و قالب‌ها

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


کار router در Ember.js، نگاشت آدرس درخواستی توسط کاربر، به یک route یا مسیریابی تعریف شده‌است. به این ترتیب مدل، کنترلر و قالب آن route به صورت خودکار بارگذاری و پردازش خواهند.
با مراجعه به ریشه‌ی سایت، فایل index.html بارگذاری می‌شود.


در اینجا تصویری از صفحه‌ی آغازین بلاگ را مشاهده می‌کنید. در آن صفحه‌ی posts همان ریشه‌ی سایت نیز می‌باشد. بنابراین نیاز است ابتدا مسیریابی آن‌را تعریف کرد. برای این منظور، فایل جدید Scripts\App\router.js را به پوشه‌ی App اضافه کنید؛ با این محتوا:
Blogger.Router.map(function () {
   this.resource('posts', { path: '/' });
});
علت تعریف قسمت path این است که ریشه‌ی سایت (/) نیز به مسیریابی posts ختم شود. در غیر اینصورت کاربر با مراجعه به ریشه‌ی سایت، یک صفحه‌ی خالی را مشاهده خواهد کرد؛ زیرا به صورت پیش فرض، آدرس قابل ترجمه‌ی یک صفحه، با آدرس و نام مسیریابی آن یکی است.

همچنین مدخل آن‌را نیز در فایل index.html تعریف نمائید:
 <script src="Scripts/App/blogger.js" type="text/javascript"></script>
<script src="Scripts/App/router.js" type="text/javascript"></script>
در اینجا Blogger همان شیء Application برنامه است که پیشتر در فایل Scripts\App\blogger.js تعریف کردیم. سپس به کمک متد Blogger.Router.map، اولین مسیریابی برنامه را افزوده‌ایم.


افزودن مسیریابی و قالب posts

در ادامه فایل جدید Scripts\Templates\posts.hbs را اضافه کنید. به این ترتیب قالب خالی مطالب به پوشه‌ی templates اضافه می‌شود. محتوای این فایل را به نحو ذیل تنظیم کنید:
<div class="container">
  <h1>Emeber.js blog</h1>
  <ul>
   <li>Item 1</li>
   <li>Item 2</li>
   <li>Item 3</li>
  </ul>
</div>
در اینجا دیگر نیازی به ذکر تگ script از نوع text/x-handlebars نیست.
برای بارگذاری این قالب نیاز است آن‌را به template loader توضیح داده شده در ابتدای بحث، در فایل index.html اضافه کنیم:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts'
 ]);
</script>
اکنون برنامه را اجرا کنید. به تصویر ذیل خواهید رسید که در آن افزونه‌ی Ember Inspector نیز نمایش داده شده‌است:



افزودن مسیریابی و قالب about

در ادامه قصد داریم صفحه‌ی about را اضافه کنیم. مجددا با افزودن مسیریابی آن به فایل Scripts\App\router.js شروع می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
});
سپس فایل قالب آن‌را در مسیر Scripts\Templates\about.hbs ایجاد خواهیم کرد؛ برای مثال با این محتوای فرضی:
 <h1>About Ember Blog</h1>
<p>Bla bla bla!</p>
اکنون نام این فایل را به template loader فایل index.html اضافه می‌کنیم.
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about'
 ]);
</script>
اگر از قسمت قبل به خاطر داشته باشید، ساختار کلی قالب‌های ember.js یک چنین شکلی را دارد:
 <script type="text/x-handlebars" data-template-name="about">

</script>
اسکریپت template loader، این تعاریف را به صورت خودکار اضافه می‌کند. مقدار data-template-name را نیز به نام فایل متناظر با آن تنظیم خواهد کرد و چون ember.js بر اساس ایده‌ی convention over configuration کار می‌کند، مسیریابی about با کنترلری به همین نام و قالبی هم نام پردازش خواهد شد. بنابراین نام فایل‌های قالب را باید بر اساس مسیریابی‌های متناظر با آن‌ها تعیین کرد.
برای آزمایش این مسیر و قالب جدید آن، آدرس http://localhost/#/about را بررسی کنید.


اضافه کردن منوی ثابت بالای سایت

روش اول این است که به ابتدای هر دو قالب about.hbs و posts.hbs، تعاریف منو را اضافه کنیم. مشکل این‌کار، تکرار کدها و پایین آمدن قابلیت نگهداری برنامه است. روش بهتر، افزودن کدهای مشترک بین صفحات، در قالب application برنامه است. نمونه‌ی آن‌را در مثال قسمت قبل مشاهده کرده‌اید. در اینجا چون قصد نداریم به صورت دستی قالب‌ها را به صفحه اضافه کنیم و همچنین برای ساده شدن نگهداری برنامه، قالب‌ها را در فایل‌های مجزایی قرار داده‌ایم، تنها کافی است فایل جدید Scripts\Templates\application.hbs را به پوشه‌ی قالب‌های برنامه اضافه کنیم؛ با این محتوا:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>
و سپس همانند قبل، نام فایل قالب اضافه شده را به template loader فایل index.html اضافه می‌کنیم:
<script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application'
 ]);
</script>


افزودن مسیریابی و قالب contact

برای افزودن صفحه‌ی تماس با مای سایت، ابتدا مسیریابی آن‌را در فایل Scripts\App\router.js تعریف می‌کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact');
});
سپس قالب متناظر با آن‌را به نام Scripts\Templates\contact.hbs اضافه خواهیم کرد؛ فعلا با این محتوای اولیه:
<h1>Contact</h1>
<ul>
  <li>Phone: ...</li>
  <li>Email: ...</li>
</ul>
و بعد template loader فایل index.html را از وجود آن مطلع خواهیم کرد:
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact' ]);
</script>
همچنین لینکی به مسیریابی آن‌را به فایل Scripts\Templates\application.hbs که منوی سایت در آن تعریف شده‌است، اضافه می‌کنیم:
<div class='container'>
 <nav class='navbar navbar-default' role='navigation'>
  <ul class='nav navbar-nav'>
  <li>{{#link-to 'posts'}}Posts{{/link-to}}</li>
  <li>{{#link-to 'about'}}About{{/link-to}}</li>
  <li>{{#link-to 'contact'}}Contact{{/link-to}}</li>
  </ul>
 </nav>

  {{outlet}}
</div>


تعریف مسیریابی تو در تو در صفحه‌ی contact

در حال حاضر صفحه‌ی Contact، ایمیل و شماره تماس را در همان بار اول نمایش می‌دهد. می‌خواهیم این دو را تبدیل به دو لینک جدید کنیم که با کلیک بر روی هر کدام، محتوای مرتبط، در قسمتی از همان صفحه بارگذاری شده و نمایش داده شود.
برای اینکار نیاز است مسیریابی را تو در تو تعریف کنیم:
Blogger.Router.map(function () {
  this.resource('posts', { path: '/' });
  this.resource('about');
  this.resource('contact', function () {
   this.resource('email');
   this.resource('phone');
  });
});
اگر مسیریابی‌های email و یا phone را به صورت مستقل مانند about و یا posts تعریف کنیم، با کلیک کاربر بر روی لینک متناظر با هر کدام، یک صفحه‌ی کاملا جدید نمایش داده می‌شود. اما در اینجا قصد داریم تنها قسمت کوچکی از همان صفحه‌ی contact را با محتوای ایمیل و یا شماره تماس جایگزین نمائیم. به همین جهت مسیریابی‌های متناظر را در اینجا به صورت تو در تو و ذیل مسیریابی contact تعریف کرده‌ایم.

پس از آن دو فایل قالب جدید Scripts\Templates\email.hbs را با محتوای:
 <h2>Email</h2>
<p>
<span></span> Email name@site.com.
</p>
و فایل قالب Scripts\Templates\phone.hbs را با محتوای:
 <h2>Phone</h2>
<p>
<span></span> Call 12345678.
</p>
به پوشه‌ی قالب‌ها اضافه نمائید به همراه معرفی نام آن‌ها به template loader برنامه در صفحه‌ی index.html :
 <script>
 EmberHandlebarsLoader.loadTemplates([
 'posts', 'about', 'application', 'contact', 'email', 'phone' ]);
</script>
اکنون به قالب contact.hbs مجددا مراجعه کرده و تعاریف آن را به نحو ذیل تغییر دهید:
<h1>Contact</h1>
<div class="row">
  <div class="col-md-6">
   <p>
    Want to get in touch?
    <ul>
      <li>{{#link-to 'phone'}}Phone{{/link-to}}</li>
      <li>{{#link-to 'email'}}Email{{/link-to}}</li>
    </ul>
   </p>
  </div>
  <div class="col-md-6">
   {{outlet}}
  </div>
</div>
در اینجا دو لینک به مسیریابی‌های Phone و Email تعریف شده‌اند. همچنین {{outlet}} نیز قابل مشاهده است. با کلیک بر روی لینک Phone، مسیریابی آن فعال شده و سپس قالب متناظر با آن در قسمت {{outlet}} رندر می‌شود. در مورد لینک Email نیز به همین نحو رفتار خواهد شد.


در اینجا نحوه‌ی پردازش مسیریابی contact را ملاحظه می‌کنید. ابتدا درخواستی جهت مشاهده‌ی آدرس http://localhost/#/contact دریافت می‌شود. سپس router این درخواست را به مسیریابی همنامی منتقل می‌کند. این مسیریابی ابتدا قالب عمومی application را رندر کرده و سپس قالب اصلی و همنام مسیریابی جاری یا همان contact.hbs را رندر می‌کند. در این صفحه چون مسیریابی تو در تویی تعریف شده‌است، اگر درخواستی برای مشاهده‌ی http://localhost/#/contact/phone دریافت شود، محتوای آن‌را در {{outlet}} قالب contact.hbs جاری رندر می‌کند.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_01.zip
پاسخ به بازخورد‌های پروژه‌ها
خطا در زمان تایید پرداخت
ببینید، زمانیکه درخواست برای پرداخت به بانک ارسال میشه، یک رکورد که شامل زمان ایجاد هست، در دیتابیس (فروشگاه) ثبت میشه. سپس کاربر (پرداخت کننده) به وب سایت بانکی میره (پرداخت و یا انصراف). هنگامیکه برمیگرده به وب سایت فروشنده، رکورد بازیابی میشه و تاریخ ایجادش بررسی میشه. در صورتیکه از زمان تاریخ ایجاد تا تاریخ فعلی (که بر اساس تایم سرور هستن و نه کلاینت)، بیش از ۲۰ دقیقه (زمان استاندارد پرداخت) گذشته باشه، پیغامی که شما گفتید، به کاربر داده میشه.
در واقع تنها حالت ممکن برای نمایش این پیغام، همین سناریو هست که توضیح دادم.
به همین علت هست که میگم این پیغام با وصل بودن یا نبودن vpn،  ارتباطی نداره.
زمان و تاریخ سرور اصلا هیچ ارتباطی به اینکه پرداخت کننده vpn داشته باشه یا نداشته باشه نداره. پرداخت کننده چه با vpn و یا چه بدون vpn زمانیکه به فروشگاه شما (سرور شما) میاد، تاریخ مربوط به سرور ثبت میشه. ASP.NET زمان رو به وقت محلی سرور ثبت میکنه و اهمیتی نداره کاربر به چه صورتی وصل شده به سرور.
اگر سرور شما به vpn متصل بود (!!) ، اون موقع حرف شما صحت پیدا میکرد.