طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: ده دقیقه

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



پیشنیازهای نمایش اطلاعات گرید به همراه صفحه بندی اطلاعات

در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانه‌های ثالث» نحوه‌ی نصب و معرفی کتابخانه‌ی ngx-bootstrap را بررسی کردیم. دقیقا همان مراحل، در اینجا نیز باید طی شوند و از این مجموعه تنها به کامپوننت Pagination آن نیاز داریم. همان قسمت ذیل گرید تصویر فوق که شماره صفحات را جهت انتخاب، نمایش داده‌است.
بنابراین ابتدا فرض بر این است که دو بسته‌ی بوت استرپ و ngx-bootstrap را نصب کرده‌اید:
> npm install bootstrap --save
> npm install ngx-bootstrap --save
در فایل angular-cli.json. شیوه‌نامه‌ی بوت استرپ را نیز افزوده‌اید:
  "apps": [
    {
      "styles": [
    "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
      ],
پس از آن باید به‌خاطر داشت که کامپوننت نمایش صفحه بندی این مجموعه PaginationModule نام دارد و باید در نزدیک‌ترین ماژول مورد نیاز، ثبت و معرفی شود:
import { PaginationModule } from "ngx-bootstrap";

@NgModule({
  imports: [
    PaginationModule.forRoot()
  ]
برای نمونه در این مثال، ماژولی به نام simple-grid.module.ts دربرگیرنده‌ی گرید مطلب جاری است و به صورت ذیل به برنامه اضافه شده‌است:
 >ng g m SimpleGrid -m app.module --routing
بنابراین تعریف PaginationModule باید به قسمت imports این ماژول اضافه شود و تعریف آن در app.module.ts تاثیری بر روی این قسمت نخواهد داشت.

کامپوننتی هم که مثال جاری را نمایش می‌دهد به صورت ذیل به ماژول SimpleGrid فوق اضافه شده‌است:
 >ng g c SimpleGrid/products-list


تهیه معادل‌های قراردادهای سمت سرور در سمت Angular

در قسمت قبل، تعدادی قرارداد مانند پارامترهای دریافتی از سمت کلاینت و ساختار اطلاعات ارسالی به سمت کلاینت را تعریف کردیم. اکنون جهت کار strongly typed با آن‌ها در سمت یک برنامه‌ی تایپ اسکریپتی Angular، کلاس‌های معادل آن‌ها را تهیه می‌کنیم.

ساختار شیء محصول دریافتی از سمت سرور
 >ng g cl SimpleGrid/app-product
با این محتوا
export class AppProduct {
  constructor(
    public productId: number,
    public productName: string,
    public price: number,
    public isAvailable: boolean
  ) {}
}
که در اینجا هر کدام از خواص ذکر شده، معادل camel case نمونه‌ی سمت سرور خود هستند (چون JSON.NET در ASP.NET Core، به صورت پیش فرض یک چنین خروجی را تولید می‌کند).

ساختار معادل پارامترهای صفحه بندی و مرتب سازی ارسالی به سمت سرور
 >ng g cl SimpleGrid/PagedQueryModel
با این محتوا
export class PagedQueryModel {
  constructor(
    public sortBy: string,
    public isAscending: boolean,
    public page: number,
    public pageSize: number
  ) {}
}
در اینجا همان ساختار IPagedQueryModel سمت سرور را مشاهده می‌کنید. از آن جهت مشخص سازی جزئیات صفحه بندی و نحوه‌ی مرتب سازی اطلاعات، استفاده می‌شود.

ساختار معادل اطلاعات صفحه بندی شده‌ی دریافتی از سمت سرور
 >ng g cl SimpleGrid/PagedQueryResult
با این محتوا
export class PagedQueryResult<T> {
  constructor(public totalItems: number, public items: T[]) {}
}
این ساختار جنریک نیز دقیقا معادل همان PagedQueryResult سمت سرور است و حاوی تعداد کل ردیف‌های یک کوئری و تنها قسمتی از اطلاعات صفحه بندی شده‌ی آن می‌باشد.

ساختار ستون‌های گرید نمایشی
 >ng g cl SimpleGrid/GridColumn
با این محتوا
export class GridColumn {
  constructor(
    public title: string,
    public propertyName: string,
    public isSortable: boolean
  ) {}
}
هر ستون نمایش داده شده، دارای یک برچسب، خاصیتی مشخص در سمت سرور و بیانگر قابلیت مرتب سازی آن می‌باشد. اگر isSortable به true تنظیم شود، با کلیک بر روی سرستون‌ها می‌توان اطلاعات را بر اساس آن ستون، مرتب سازی کرد.


تهیه سرویس ارسال اطلاعات صفحه بندی به سرور و دریافت اطلاعات از آن

پس از تدارک این مقدمات، اکنون کار تعریف سرویسی که این اطلاعات را به سمت سرور ارسال می‌کند و نتیجه را باز می‌گرداند، به صورت ذیل خواهد بود:
 >ng g s SimpleGrid/products-list -m simple-grid.module
این دستور سبب ایجاد کلاس ProductsListService شده و همچنین قسمت providers ماژول simple-grid را نیز بر این اساس به روز رسانی می‌کند.
پیش از تکمیل این سرویس، نیاز است متدی را جهت تبدیل یک شیء، به معادل کوئری استرینگ آن تهیه کنیم:
  toQueryString(obj: any): string {
    const parts = [];
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        const value = obj[key];
        if (value !== null && value !== undefined) {
          parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
        }
      }
    }
    return parts.join("&");
  }
در قسمت قبل امضای متد GetPagedProducts دارای ویژگی HttpGet است. بنابراین، نیاز است اطلاعات را به صورت کوئری استرینگ از سمت کلاینت دریافت کند و متد toQueryString فوق به صورت خودکار بر روی تمام خواص یک شیء دلخواه حرکت کرده و آن‌ها را تبدیل به یک رشته‌ی حاوی کوئری استرینگ‌ها می‌کند.
[HttpGet("[action]")]
public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
برای نمونه متد toQueryString فوق است که سبب ارسال یک چنین درخواستی به سمت سرور می‌شود:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7

پس از این تعریف، سرویس ProductsListService  به صورت ذیل تکمیل خواهد شد:
@Injectable()
export class ProductsListService {
  private baseUrl = "api/Product";

  constructor(private http: Http) {}

  getPagedProductsList(
    queryModel: PagedQueryModel
  ): Observable<PagedQueryResult<AppProduct>> {
    return this.http
      .get(`${this.baseUrl}/GetPagedProducts?${this.toQueryString(queryModel)}`)
      .map(res => {
        const result = res.json();
        return new PagedQueryResult<AppProduct>(
          result.totalItems,
          result.items
        );
      });
  }
در اینجا از متد toQueryString، جهت تکمیل متد get ارسالی به سمت سرور استفاده شده‌است تا پارامترها را به صورت کوئری استرینگ‌ها تبدیل کرده و ارسال کند.
سپس در متد map آن، res.json دقیقا همان ساختار PagedQueryResult سمت سرور را به همراه دارد. اینجا است که فرصت خواهیم داشت نمونه‌ی سمت کلاینت آن‌را که در ابتدای بحث تهیه کردیم، وهله سازی کرده و بازگشت دهیم (نگاشت فیلدهای دریافتی از سمت سرور به سمت کلاینت).


تکمیل کامپوننت نمایش گرید

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


  <table class="table table-striped table-hover table-bordered table-condensed">
    <thead>
      <tr>
        <th class="text-center" style="width:3%">#</th>
        <th *ngFor="let column of columns" class="text-center">
          <div *ngIf="column.isSortable" (click)="sortBy(column.propertyName)" style="cursor: pointer">
            {{ column.title }}
            <i *ngIf="queryModel.sortBy === column.propertyName" class="glyphicon"
              [class.glyphicon-sort-by-order]="queryModel.isAscending" [class.glyphicon-sort-by-order-alt]="!queryModel.isAscending"></i>
          </div>
          <div *ngIf="!column.isSortable" style="cursor: pointer">
            {{ column.title }}
          </div>
        </th>
      </tr>
    </thead>
در اینجا ابتدا بررسی می‌شود که آیا یک ستون قابلیت مرتب سازی را دارد، یا خیر؟ اگر اینطور است، در کنار آن یک گلیف آیکن مرتب سازی درج می‌شود. اگر خیر، صرفا متن عنوان آن نمایش داده خواهد شد. می‌شد تمام این موارد را به ازای هر ستون به صورت مجزایی ارائه داد، اما در این حالت به کدهای تکراری زیادی می‌رسیدیم. به همین جهت از یک حلقه بر روی تعریف ستون‌های این گرید استفاده شده‌است. آرایه‌ی این ستون‌ها نیز به صورت ذیل تعریف می‌شود:
export class ProductsListComponent implements OnInit {
  columns: GridColumn[] = [
    new GridColumn("Id", "productId", true),
    new GridColumn("Name", "productName", true),
    new GridColumn("Price", "price", true),
    new GridColumn("Available", "isAvailable", true)
  ];

همچنین در کدهای قالب این کامپوننت، مدیریت کلیک بر روی یک سر ستون را نیز مشاهده می‌کنید:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);

  sortBy(columnName) {
    if (this.queryModel.sortBy === columnName) {
      this.queryModel.isAscending = !this.queryModel.isAscending;
    } else {
      this.queryModel.sortBy = columnName;
      this.queryModel.isAscending = true;
    }
    this.getPagedProductsList();
  }
}
در این‌حالت اگر ستونی که بر روی آن کلیک شده، پیشتر مرتب سازی شده‌است، صرفا خاصیت صعودی بودن آن برعکس خواهد شد. در غیراینصورت، نام خاصیت درخواستی مرتب سازی و جهت آن نیز مشخص می‌شود. سپس مجددا این گرید توسط متد getPagedProductsList رندر خواهد شد.

کار رندر بدنه‌ی اصلی گرید توسط همین چند سطر در قالب آن مدیریت می‌شود:
    <tbody>
      <tr *ngFor="let item of queryResult.items; let i = index">
        <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td>
        <td class="text-center">{{ item.productId }}</td>
        <td class="text-center">{{ item.productName }}</td>
        <td class="text-center">{{ item.price | number:'.0' }}</td>
        <td class="text-center">
          <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable"
            disabled="disabled" />
        </td>
      </tr>
    </tbody>
  </table>
اولین ستون آن، اندکی ابتکاری است. در اینجا شماره ردیف‌های خودکاری در هر صفحه درج خواهند شد. این شماره ردیف نیز جزو ستون‌های منبع داده‌ی فرضی برنامه نیست. به همین جهت برای درج آن، توسط let i = index در ngFor، به شماره ایندکس ردیف جاری دسترسی پیدا می‌کنیم. سپس توسط محاسباتی بر اساس تعداد ردیف‌های هر صفحه و شماره‌ی صفحه‌ی جاری، می‌توان شماره ردیف فعلی را محاسبه کرد.

در اینجا حلقه‌ای بر روی queryResult.items تشکیل شده‌است. این منبع داده به صورت ذیل در کامپوننت متناظر مقدار دهی می‌شود:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;
  isLoading = false;
  queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage);
  queryResult = new PagedQueryResult<AppProduct>(0, []);

  constructor(private productsListService: ProductsListService) {}

  ngOnInit() {
    this.getPagedProductsList();
  }

  private getPagedProductsList() {
    this.isLoading = true;
    this.productsListService
      .getPagedProductsList(this.queryModel)
      .subscribe(result => {
        this.queryResult = result;
        this.isLoading = false;
      });
  }
}
ابتدا سرویس ProductsListService را که در ابتدای بحث تکمیل شد، به سازنده‌ی این کامپوننت تزریق می‌کنیم. به کمک آن می‌توان در متد getPagedProductsList، ابتدا queryModel جاری را که شامل اطلاعات مرتب سازی و صفحه بندی است، به سرور ارسال کرده و سپس نتیجه‌ی نهایی را به queryResult انتساب دهیم. به این ترتیب تعداد کل رکوردها و همچنین آیتم‌های صفحه‌ی جاری دریافت می‌شوند. اکنون حلقه‌ی ngFor نمایش بدنه‌ی گرید، کار تکمیل صفحه‌ی جاری را انجام خواهد داد.

قسمت آخر کار، افزودن کامپوننت نمایش شماره صفحات است:


  <div align="center">
    <pagination [maxSize]="8" [boundaryLinks]="true" [totalItems]="queryResult.totalItems"
      [rotate]="false" previousText="&lsaquo;" nextText="&rsaquo;" firstText="&laquo;"
      lastText="&raquo;" (numPages)="numberOfPages = $event" [(ngModel)]="currentPage"
      (pageChanged)="onPageChange($event)"></pagination>
  </div>
  <pre class="card card-block card-header">Page: {{currentPage}} / {{numberOfPages}}</pre>
در اینجا از کامپوننت pagination مجموعه‌ی ngx-bootstarp استفاده شده‌است و یک سری از خواص مستند شده‌ی آن‌، مقدار دهی شده‌اند؛ مانند متن‌های صفحه‌ی بعد و قبل و امثال آن. مدیریت کلیک بر روی شماره‌های آن، در کامپوننت جاری به صورت ذیل است:
export class ProductsListComponent implements OnInit {
  itemsPerPage = 7;
  currentPage: number;
  numberOfPages: number;

  onPageChange(event: any) {
    this.queryModel.page = event.page;
    this.getPagedProductsList();
  }
}
علت تعریف دو خاصیت اضافه‌ی currentPage و numberOfPages، استفاده‌ی از آن‌ها در قسمت ذیل این شماره‌ها (خارج از کامپوننت نمایش شماره صفحات) جهت نمایش page 1/x است.
هر زمانیکه کاربر بر روی شما‌ره‌ای کلیک می‌کند، رخ‌داد onPageChange فراخوانی شده و در این‌حالت تنها کافی است شماره صفحه‌ی درخواستی queryModel جاری را به روزرسانی کرده و سپس آن‌را در اختیار متد getPagedProductsList جهت دریافت اطلاعات این صفحه‌ی درخواستی قرار دهیم.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
  • #
    ‫۶ سال و ۱۰ ماه قبل، یکشنبه ۲۱ آبان ۱۳۹۶، ساعت ۱۴:۰۸
    سلام؛ برای ایجاد فیلتر روی این گرید به چه صورت باید اقدام کرد؟
    • #
      ‫۶ سال و ۱۰ ماه قبل، دوشنبه ۲۲ آبان ۱۳۹۶، ساعت ۱۸:۰۲
      پیاده سازی جستجوی بر روی این گرید، شامل موارد زیر است:
      اضافه کردن دو خاصیت جدید به کلاس PagedQueryModel سمت کلاینت جهت مشخص سازی ستونی که قرار است بر روی آن جستجو انجام شود و همچنین مقدار آن:
      export class PagedQueryModel {
        constructor(
          // ...
          public filterByColumn: string,
          public filterByValue: string,
        ) { }
      }
      سپس به ProductsListComponent دو متد زیر را اضافه می‌کنیم:
        doFilter() {
          this.queryModel.page = 1;
          this.getPagedProductsList();
        }
      
        resetFilter() {
          this.queryModel.page = 1;
          this.queryModel.filterByColumn = "";
          this.queryModel.filterByValue = "";
          this.getPagedProductsList();
        }
      اولی کار جستجو را انجام می‌دهد و دومی بازگشت حالت گرید به وضعیت اول آن است. متد getPagedProductsList قابلیت واکشی خودکار اطلاعات دو خاصیت جدیدی را که اضافه کردیم دارد و نیازی به تنظیمات اضافه‌تری ندارد. یعنی filterByColumn و filterByValue را به صورت خودکار به سمت سرور ارسال می‌کند.

      پس از آن، قالب این گرید (products-list.component.html) جهت افزودن جستجو، به صورت زیر تغییر می‌کند:
      <div class="panel panel-default">
        <div class="panel-body">
          <div class="form-group">
            <input type="text" [(ngModel)]="queryModel.filterByValue" placeholder="Search For ..."
              class="form-control" />
          </div>
          <div class="form-group">
            <select class="form-control" name="filterColumn" [(ngModel)]="queryModel.filterByColumn">
              <option value="">Filter by ...</option>
              <option *ngFor="let column of columns" [value]="column.propertyName">
                {{ column.title }}
              </option>
            </select>
          </div>
          <button class="btn btn-primary" type="button" (click)="doFilter()">Search</button>
          <button class="btn btn-default" type="button" (click)="resetFilter()">Reset</button>
        </div>
      </div>
      که در آن queryModel.filterByColumn و queryModel.filterByValue از کاربر دریافت می‌شوند. همچنین دو متد doFilter و resetFilter را نیز فراخوانی می‌کند.
      با این شکل:


      تغییرات سمت سرور آن نیز به صورت ذیل است:
      ابتدا IPagedQueryModel را با همان دو خاصیت جدید ستون فیلتر شونده و مقدار آن، تکمیل می‌کنیم:
          public interface IPagedQueryModel
          {
          // ....
              string FilterByColumn { get; set; }
              string FilterByValue { get; set; }
          }
      
          public class ProductQueryViewModel : IPagedQueryModel
          {
              // ... other properties ...
      
      // ...
              public string FilterByColumn { get; set; }
              public string FilterByValue { get; set; }
          }
      از این دو خاصیت جدید، جهت افزودن متد اعمال جستجو، همانند متد ApplyOrdering که پیشتر تعریف شد، استفاده می‌کنیم:
          public static class IQueryableExtensions
          {
              public static IQueryable<T> ApplyFiltering<T>(
                this IQueryable<T> query,
                IPagedQueryModel model,
                IDictionary<string, Expression<Func<T, object>>> columnsMap)
              {
                  if (string.IsNullOrWhiteSpace(model.FilterByValue) || !columnsMap.ContainsKey(model.FilterByColumn))
                  {
                      return query;
                  }
      
                  var func = columnsMap[model.FilterByColumn].Compile();
                  return query.Where(x => func(x).ToString() == model.FilterByValue);
              }
      در اینجا همان columnsMap مورد استفاده در متد ApplyOrdering جهت نگاشت نام‌های رشته‌ای ستون‌ها به معادل Expression آن‌ها استفاده شده‌است.

      در آخر، به کنترلر ProductController و اکشن متد GetPagedProducts آن مراجعه کرده و پیش از ApplyOrdering، متد جدید ApplyFiltering فوق را اضافه می‌کنیم:
      var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
                  {
                      ["productId"] = p => p.ProductId,
                      ["productName"] = p => p.ProductName,
                      ["isAvailable"] = p => p.IsAvailable,
                      ["price"] = p => p.Price
                  };
      query = query.ApplyFiltering(queryModel, columnsMap);
      query = query.ApplyOrdering(queryModel, columnsMap);

      کدهای کامل این تغییرات را از اینجا می‌توانید دریافت کنید.
      • #
        ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۲۳ آبان ۱۳۹۶، ساعت ۱۳:۵۲
        فیلتر نوشته شده در متد ApplyFiltering  فقط داده هایی که دقیقا برابر با رشته ارسال شده باشند را باز می‌گرداند ، برای اعمال فیلترینگ بصورت شامل (Like) این قطعه کد را جایگزین متد  ApplyFiltering   کنید :
        public static IQueryable<T> ApplyFiltering<T>(
                  this IQueryable<T> query,
                  IPagedQueryModel model,
                  IDictionary<string, Expression<Func<T, object>>> columnsMap)
                {
                    if (string.IsNullOrWhiteSpace(model.FilterByValue) || !columnsMap.ContainsKey(model.FilterByColumn))
                    {
                        return query;
                    }
        
                    var func = columnsMap[model.FilterByColumn].Compile();
                    return query.Where(x => func(x).ToString().Contains( model.FilterByValue.Trim()));
                }


        • #
          ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۲۳ آبان ۱۳۹۶، ساعت ۱۴:۱۲
          برای حالت‌های پیشرفته‌تر بهتر است columnsMap را با System.Linq.Dynamic.Core جایگزین کنید و همچنین نوع مقایسه را هم از کاربر دریافت کنید (برای مثال حالت‌های مساوی، مخالف، شروع شده با، تمام شده با، حاوی عبارت و غیره).
      • #
        ‫۴ سال و ۳ ماه قبل، چهارشنبه ۷ خرداد ۱۳۹۹، ساعت ۲۱:۳۳
        سلام.
        در net core 3.1 قسمت 
        var func = columnsMap[model.FilterByColumn].Compile();
        با خطای 
        {Method = <Internal Error evaluating expression>}
        مواجه میشه، ممنون میشم راهنمایی بفرمایید.
        • #
          ‫۴ سال و ۳ ماه قبل، دوشنبه ۱۲ خرداد ۱۳۹۹، ساعت ۱۵:۴۶
          بهتر است متد ApplyFiltering به این صورت بازنویسی شود تا قسمت predicate آن‌را بتوان ساده‌تر تغییر داد (نمونه‌اش در Kendo.DynamicLinq.Core استفاده شده):
                  public static IQueryable<T> ApplyFiltering<T>(
                    this IQueryable<T> query,
                    IPagedQueryModel model,
                    IDictionary<string, Expression<Func<T, object>>> columnsMap)
                  {
                      if (string.IsNullOrWhiteSpace(model.FilterByValue) || !columnsMap.ContainsKey(model.FilterByColumn))
                      {
                          return query;
                      }
          
                      var predicate = string.Format("{0} {1} @{2}", model.FilterByColumn, "==", 0);
                      return query.Where(predicate, model.FilterByValue);
                  }
          که از System.Linq.Dynamic.Core استفاده می‌کند:
            <ItemGroup>
              <PackageReference Include="System.Linq.Dynamic.Core" Version="1.1.2" />
            </ItemGroup>
  • #
    ‫۶ سال و ۱۰ ماه قبل، دوشنبه ۲۲ آبان ۱۳۹۶، ساعت ۲۰:۰۳
    سلام؛ متاسفانه صفحه بندی مشکل داره ، اگه itemsPerPage   را تغییر بدیم متاسفانه تعداد صفحه‌ها کم و زیاد نمیشه . روی مثال خودتون 1500 رکورد اطلاعات هست اگه itemsPerPage   بشه 10 تعداد صفحه‌ها 150 هست و اگه بشه 5 هم 150 هست و تا رکورد 750 نمایش میده.  
    • #
      ‫۶ سال و ۱۰ ماه قبل، سه‌شنبه ۲۳ آبان ۱۳۹۶، ساعت ۱۶:۰۹
      علت اینجا است که [itemsPerPage] کامپوننت pagination هم باید مقدار دهی شود.
  • #
    ‫۶ سال و ۸ ماه قبل، جمعه ۲۲ دی ۱۳۹۶، ساعت ۱۳:۳۲
    آیا می‌شود کلاس‌های تایپ اسکریپتی AppProduct و  PagedQueryModel و  GridColumn  ، را به صورت interface پیاده سازی نمود ؟
    • #
      ‫۶ سال و ۸ ماه قبل، جمعه ۲۲ دی ۱۳۹۶، ساعت ۱۳:۳۵
      چیزی را امتحان کردید؟ در این بین به چه مشکلی برخوردید؟
      • #
        ‫۶ سال و ۷ ماه قبل، شنبه ۷ بهمن ۱۳۹۶، ساعت ۱۶:۰۰
        به مشکلی بر نخوردم.
        چون برای نمایش فیلد‌های جدول در angular هم میشه از interface استفاده کرد و هم میشه از class استفاده کرد ، میخواستم بدونم آیا فرقی دارن.
        export class AppProduct {
            constructor(
              public productId: number,
              public productName: string,
              public price: number,
              public isAvailable: boolean
            ) {}
          }
        یا
        export class AppProduct {
            public productId: number;
            public productName: string;
            public price: number;
            public isAvailable: boolean;
        }
        یا
        export interface AppProduct {
            productId: number;
            productName: string;
            price: number;
            isAvailable: boolean;
        }

        • #
          ‫۶ سال و ۷ ماه قبل، یکشنبه ۸ بهمن ۱۳۹۶، ساعت ۱۴:۰۷
          در راهنمای کدنویسی Angular  توصیه شده از اینترفیس‌ها برای تعریف نوع مدل‌ها استفاده شود:

          Consider using a class instead of an interface for services and declarables (components, directives, and pipes)

          Consider using an interface for data models

          البته این مورد موافق‌ها و مخالف‌هایی هم دارد.
          در کل سلیقه‌ای است و بهتر است یکنواختی و یک‌دستی کد مدنظر باشد.
          • #
            ‫۶ سال و ۱ ماه قبل، چهارشنبه ۲۴ مرداد ۱۳۹۷، ساعت ۱۵:۲۴
            برای مقدار پیش فرض به اینترفیس‌ها در پروژ‌های خیلی بزرگ کدام الگو را انجام دهیم بهتر است؟
            interface student {
                name: string,
                phone: any
            }
            این روش 
            student = {
                name :'hasan',
                phone :123
            }
            constructor() {}
            یا از این روش
            model: studnet= {} as any;
            constructor() {
             model.name='hasan';
             model.phone =123;
            }

            • #
              ‫۶ سال و ۱ ماه قبل، چهارشنبه ۲۴ مرداد ۱۳۹۷، ساعت ۱۵:۳۹
              - این نوع سؤالات مرتبط به مطلب «مبانی TypeScript؛ اینترفیس‌ها» هستند.
              - در روش اول، نوع شیء تعریف شده any است (نوعی ندارد). اگر پروژه بزرگ است و اگر هدف عیب‌یابی سریعتر است با TypeScript، در حالت استفاده از any، از مزایای اینترفیس تعریف شده استفاده نمی‌کنید. در این حالت TypeScript کمکی به شما نخواهد کرد.
              - حالت دوم هم آنچنان مرسوم نیست در TypeScript. حالت مرسوم همان روش اول است که دقیقا نوع آن هم مشخص شده باشد.
  • #
    ‫۶ سال و ۵ ماه قبل، دوشنبه ۲۰ فروردین ۱۳۹۷، ساعت ۱۳:۴۳
    سلام
    در صورتی که نیاز به ایجاد ستون‌های جدول توسط ngFor از روی لیست GridColumn در html  باشد چه ایده ای پیشنهاد میکنید.
    (این مورد برای ایجاد امکان جابجایی و عدم نمایش ستون‌ها توسط کاربر مورد استفاده قرار میگیرد)
  • #
    ‫۵ سال قبل، سه‌شنبه ۲۹ مرداد ۱۳۹۸، ساعت ۱۹:۱۸
    میشه زیر ساختی که تو قسمت قبلی تهیه کردین رو  بدون استفاده از انگولار هم پیاده سازی کرد؟ فقط با استفاده از بوت استرپ و ViewEngine خود دات نت کور؟  تو سایت من هر چی گشتم نمونه مشابه پیدا نکردم. البته بدون وابستگی به کتابخانه ای از nuget .
  • #
    ‫۴ سال و ۱ ماه قبل، پنجشنبه ۱۶ مرداد ۱۳۹۹، ساعت ۰۴:۱۹
    به جای متد toQueryString از کلاس HttpParams برای ارسال مقادیر در کوئری استرینگ نیز میتوان استفاده کرد به شکل زیر :

    let params = new HttpParams();
    if(sortBy != null && isAscending != null && page != null && pageSize != null) {
       params = params.append('sortBy');
       params = params.append('isAscending');
       params = params.append('page');
       params = params.append('pageSize');
    }
    و سپس این مقادیر رو به به سمت کلاینت فرستاد درون سرویس ProductsListService به شکل زیر :

    getPagedProductsList(sortBy?,isAscending?,
                page?,pageSize?):Observable<PagedQueryResult<AppProduct>> {
           let params = new HttpParams();
           if(sortBy != null && isAscending != null && page != null && pageSize != null) 
           {
             params = params.append('sortBy');
             params = params.append('isAscending');
             params = params.append('page');
             params = params.append('pageSize');
           }
    
          return this.http.get(`${this.baseUrl}/GetPagedProducts , {params})
          .map(res => {
            const result = res.json();
            return new PagedQueryResult<AppProduct>(
              result.totalItems,
              result.items
            );
          });
      }