در قسمتهای قبل ( ^ ، ^ و ^ ) GraphQL را در ASP.Net Core راه اندازی کردیم و در قسمت ( فراخوانی GraphQL API در یک کلاینت ASP.NET Core ) از GraphQL API فراهم شده در یک کلاینت ASP Net Core استفاده کردیم. اکنون میخواهیم چگونگی استفاده از GraphQL را در انگیولار، یاد بگیریم.
Apollo Angular، به شما اجازه میدهد دادهها را از یک سرور GraphQL دریافت و از آن برای ساختن UI های واکنشی و پیچیده در انگیولار استفاده کنید. وقتی که از Apollo Client استفاده میکنیم، نیازی نیست هیچ چیز خاصی را در مورد سینتکس query ها یادبگیریم؛ به دلیل اینکه همه چیز همان استاندارد GraphQL میباشد. هر چیزی را که شما در GraphQL query IDE تایپ میکنید، میتوانید آنها را در کدهای Apollo Client نیز قرار دهید.
Installation with Angular Schematics
بعد از ایجاد یک پروژه انگیولار با دستور زیر
ng new apollo-angular-project
ng add apollo-angular
const uri = 'https://localhost:5001/graphql';
اکنون همه چیز تمام شدهاست. شما میتوانید اولین query خود را اجرا کنید.
Installation without Angular Schematics
اگر میخواهید Apollo را بدون کمک گرفتن از Angular Schematics نصب کنید، در ابتدا کتابخانههای زیر را نصب نمائید:
npm install --save apollo-angular \ apollo-angular-link-http \ apollo-link \ apollo-client \ apollo-cache-inmemory \ graphql-tag \ graphql
{ "compilerOptions": { // ... "lib": [ "es2017", "dom", "esnext.asynciterable" ] } }
در ادامه، فایل app.module.ts را باز کرده و آن را مطابق زیر ویرایش کنید:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from "@angular/common/http"; import { ApolloModule, APOLLO_OPTIONS } from "apollo-angular"; import { HttpLinkModule, HttpLink } from "apollo-angular-link-http"; import { InMemoryCache } from "apollo-cache-inmemory"; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, ApolloModule, HttpLinkModule ], providers: [ { provide: APOLLO_OPTIONS, useFactory: (httpLink: HttpLink) => { return { cache: new InMemoryCache(), link: httpLink.create({ uri: "https://localhost:5001/graphql" }) } }, deps: [ HttpLink ] }], bootstrap: [ AppComponent ] }) export class AppModule { }
- با استفاده از سرویس apollo-angular-link-http و HttpLink، کلاینت را به یک سرور GraphQL متصل میکنیم.
- apollo-cache-inmemory و InMemoryCache محلی برای ذخیره سازی دادهها میباشد.
- APOLLO_OPTIONS فراهم کننده تنظیمات Apollo Client است.
- HttpLink نیاز به HttpClient دارد. به همین خاطر است که ما از HttpClientModule استفاده کردهایم.
Links and Cache
Apollo Client، یک لایه واسط شبکه قابل تعویض را دارد که به شما اجازه میدهد تا تنظیم کنید که چگونه query ها در HTTP ارسال شوند؛ یا کل بخش Network را با چیزی کاملا سفارشی سازی شده جایگزین کنید؛ مثل یک websocket transport.
apollo-angular-link-http : از Http برای ارسال query ها استفاده میکند.
apollo-cache-inmemory : پیاده سازی کش پیش فرض برای Apollo Client 2.0 میباشد.
نکته
Apollo یک سرویس export شده انگیولار از apollo-angular، برای به اشتراک گذاشتن دادههای GraphQL با UI شما میباشد.
شروع کار
به همان روش که فایلهای Model را برای کلاینت ASP.NET Core ایجاد کردیم، در اینجا هم ایجاد میکنیم. کار را با ایجاد کردن یک پوشه جدید به نام types، شروع میکنیم و چند type را در آن تعریف خواهیم کرد ( OwnerInputType ،AccountType و OwnerType ):
export type OwnerInputType = { name: string; address: string; }
export type AccountType = { 'id': string; 'description': string; 'ownerId' : string; 'type': string; }
import { AccountType } from './accountType'; export type OwnerType = { 'id': string; 'name': string; 'address': string; 'accounts': AccountType[]; }
سپس یک سرویس را به نام graphql ایجاد میکنیم:
ng g s graphql
و آن را همانند زیر ویرایش میکنیم:
import { Injectable } from '@angular/core'; import { Apollo } from 'apollo-angular'; import gql from 'graphql-tag'; @Injectable({ providedIn: 'root' }) export class GraphqlService { constructor(private apollo: Apollo) { } }
اکنون همه چیز آماده است تا تعدادی query و mutation را اجرا کنیم ( providedIn ).
بازیابی تمامی Owner ها
سرویس graphql را باز میکنیم و آن را همانند زیر ویرایش میکنیم ( اضافه کردن متد getOwners ):
public getOwners = () => { return this.apollo.query({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }); }
سپس فایل app.component.ts را باز کرده و همانند زیر ویرایش میکنیم:
export class AppComponent implements OnInit { public owners: OwnerType[]; public loading = true; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.graphQLService.getOwners().subscribe(result => { this.owners = result.data["owners"] as OwnerType[]; this.loading = result.loading; }); } }
و هم چنین app.component.html:
<div> <div *ngIf="!this.loading"> <table> <thead> <tr> <th> # </th> <th> نام و نام خانوادگی </th> <th> آدرس </th> </tr> </thead> <tbody> <ng-container *ngFor="let item of this.owners;let idx=index" [ngTemplateOutlet]="innertable" [ngTemplateOutletContext]="{item:item, index:idx}"></ng-container> </tbody> </table> </div> <div *ngIf="this.loading"> <p> در حال بارگذاری لیست ... </p> </div> </div> <ng-template #innertable let-item="item" let-idx="index"> <tr> <td>{{idx+1}}</td> <td>{{item.name}}</td> <td>{{item.address}}</td> </tr> <tr *ngIf="this.item.accounts && this.item.accounts.length > 0"> <td colspan="4"> <div> <p>Accounts</p> </div> <div> <table> <thead> <tr> <th>#</th> <th>نوع</th> <th>توضیحات</th> </tr> </thead> <tbody> <tr *ngFor="let innerItem of this.item.accounts;let innerIndex=index"> <td> {{innerIndex+1}} </td> <td> {{innerItem.type}} </td> <td> {{innerItem.description}} </td> </tr> </tbody> </table> </div> </td> </tr> </ng-template>
dotnet restore dotnet run
سپس پروژه را اجرا کنید:
ng serve
خروجی به صورت زیر میباشد (لیست تمامی Owner ها به همراه Accountهای مربوط به هر Owner):
در متد getOwner، بجای apollo.query میتوان از apollo.watchQuery استفاده کرد که در نمونه زیر، در ابتدا، GraphQL query را در تابع gql ( از graphql-tag ) برای خصوصیت query در متد apollo.watchQuery پاس میدهیم.
public getOwners = () => { return this.apollo.watchQuery<any>({ query: gql`query getOwners{ owners{ id, name, address, accounts{ id, description, type } } }` }) }
export class AppComponent implements OnInit { loading: boolean; public owners: OwnerType[]; private querySubscription: Subscription; constructor(private graphQLService: GraphqlService) { } ngOnInit() { this.querySubscription = this.graphQLService.getOwners() .valueChanges .subscribe(result => { this.loading = result.loading; this.owners = result.data["owners"] as OwnerType[]; }); } ngOnDestroy() { this.querySubscription.unsubscribe(); } }
مابقی query ها و mutation ها از سرویس graphql
بازیابی یک Owner مشخص
public getOwner = (id) => { return this.apollo.query({ query: gql`query getOwner($ownerID: ID!){ owner(ownerId: $ownerID){ id, name, address, accounts{ id, description, type } } }`, variables: { ownerID: id } }) }
ایجاد یک Owner جدید
public createOwner = (ownerToCreate: OwnerInputType) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!){ createOwner(owner: $owner){ id, name, address } }`, variables: { owner: ownerToCreate } }) }
ویرایش یک Owner
public updateOwner = (ownerToUpdate: OwnerInputType, id: string) => { return this.apollo.mutate({ mutation: gql`mutation($owner: ownerInput!, $ownerId: ID!){ updateOwner(owner: $owner, ownerId: $ownerId){ id, name, address } }`, variables: { owner: ownerToUpdate, ownerId: id } }) }
و در نهایت حذف یک Owner
public deleteOwner = (id: string) => { return this.apollo.mutate({ mutation: gql`mutation($ownerId: ID!){ deleteOwner(ownerId: $ownerId) }`, variables: { ownerId: id } }) }
کدهای کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کدهای کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید : ASPCoreGraphQL_3.rar
نظرات مطالب
نحوه استفاده از ViewModel در ASP.NET MVC
- در مورد اصول اولیه لایه بندی یک دوره 11 قسمتی در سایت هست. بحث مفصلی است و نیاز به پایه تئوری داره.
- مشکل بعدی کدهای شما به Html.BeginForm بر میگرده. زمانیکه پارامتری رو عنوان نکنید به اکشن متد پیش فرض مشخص شده در سیستم مسیریابی اشاره میکنه. بهتره که این موارد رو صریحا ذکر کنید تا مشکل نداشته باشید؛ مانند کد زیر. به علاوه شما نمیتونید کل یک حلقه رو داخل یک فرم قرار بدید. الان در صفحه، چندین و چند کنترل دریافت اطلاعات دارای یک Id پیدا کردید. اعضای داخل حلقه باید یک فرم را تشکیل بدن؛ در این حالت شیء ارسالی به سرور مثلا Resume خواهد بود و نه ResumeOfViewModel شما که در عمل یک لیست است. بهتر است داخل حلقه این View شلوغ رو Refactor کنید به یک Partial view با مدلی از جنس Resume تا بهتر بتونید مسایل رو مدیریت کنید.
- مشکل بعدی کدهای شما به Html.BeginForm بر میگرده. زمانیکه پارامتری رو عنوان نکنید به اکشن متد پیش فرض مشخص شده در سیستم مسیریابی اشاره میکنه. بهتره که این موارد رو صریحا ذکر کنید تا مشکل نداشته باشید؛ مانند کد زیر. به علاوه شما نمیتونید کل یک حلقه رو داخل یک فرم قرار بدید. الان در صفحه، چندین و چند کنترل دریافت اطلاعات دارای یک Id پیدا کردید. اعضای داخل حلقه باید یک فرم را تشکیل بدن؛ در این حالت شیء ارسالی به سرور مثلا Resume خواهد بود و نه ResumeOfViewModel شما که در عمل یک لیست است. بهتر است داخل حلقه این View شلوغ رو Refactor کنید به یک Partial view با مدلی از جنس Resume تا بهتر بتونید مسایل رو مدیریت کنید.
@using (Html.BeginForm(actionName: "EditResume", controllerName: "SomeName")) { }
نظرات اشتراکها
کتابخانه tinymce
Required کردن TinyMCE با اتریبیوت اعتبارسنجی سفارشی
سمت کلاینت :
نحوه انجام کار :
سمت سرور :
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)] public class MakeTinyMceRequiredAttribute : ValidationAttribute, IClientModelValidator { public MakeTinyMceRequiredAttribute() { ErrorMessage = "لطفا {0} را وارد نمایید"; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var displayName = validationContext.DisplayName; ErrorMessage = ErrorMessage.Replace("{0}", displayName); if (string.IsNullOrWhiteSpace(value?.ToString())) { return new ValidationResult(ErrorMessage); } return ValidationResult.Success; } public void AddValidation(ClientModelValidationContext context) { var displayName = context.ModelMetadata.ContainerMetadata .ModelType.GetProperty(context.ModelMetadata.PropertyName) .GetCustomAttributes(typeof(DisplayAttribute), false) .Cast<DisplayAttribute>() .FirstOrDefault()?.Name; ErrorMessage = ErrorMessage.Replace("{0}", displayName); MergeAttribute(context.Attributes, "data-val", "true"); MergeAttribute(context.Attributes, "data-val-makeTinyMceRequired", ErrorMessage); } public bool MergeAttribute(IDictionary<string, string> attributes, string key, string value) { if (attributes.ContainsKey(key)) { return false; } attributes.Add(key, value); return true; } }
if (jQuery.validator) { // For hidden inputs $.validator.setDefaults({ ignore: [] }); // makeTinyMceRequired jQuery.validator.addMethod('makeTinyMceRequired', function (value, element, param) { var editorId = $(element).attr('id'); var editorContent = tinyMCE.get(editorId).getContent(); $('body').append(`<div id="test-makeTinyMceRequired">${editorContent}</div>`); var result = isNullOrWhitespace($('#test-makeTinyMceRequired').text()); $('#test-makeTinyMceRequired').remove(); return !result; }); jQuery.validator.unobtrusive.adapters.addBool('makeTinyMceRequired'); } function isNullOrWhitespace(input) { if (typeof input === 'undefined' || input == null) return true; return input.replace(/\s/g, '').length < 1; }
متنِ ادیتور TinyMCE داخل یک div موقت ریخته میشه بعد text اون div با isNullOrWhitespace بررسی میشه، اگه کاربر مقداری رو وارد کرده باشه اعتبارسنجی پاس میشه در غیر اینصورت متن خطارو نمایش میده.
نظرات مطالب
C# 8.0 - Nullable Reference Types
بهبودهای نوعهای ارجاعی نالنپذیر در NET Core 3.0 Preview 7.
پس از نصب SDK جدید، نحوهی فعالسازی این قابلیت در فایل csproj، به صورت زیر درآمدهاست:
این موارد در Preview 7 جدید هستند:
1) امکان اضافه کردن قید جدید notnull در حین تعریف نوعهای جنریک
2) اضافه شدن یک سری ویژگی توکار جدید برای «پیش» بررسی کار با نوعهای ارجاعی نال نپذیر
ویژگی AllowNull به فراخوانها امکان ارسال نال را حتی اگر مجاز نباشد، میدهد. این ویژگی جدید در فضای نام System.Diagnostics.CodeAnalysis تعریف شدهاست. برعکس آن ویژگی DisallowNull، سبب خواهد شد تا فراخوانها حتی در صورت مجاز بودن نیز نتوانند نال ارسال کنند.
در مثال فوق ویژگی AllowNull سبب میشود تا در قسمت setter امکان دریافت نال نیز میسر شود؛ برای مثال جهت سازگاری با نگارشهای قبلی برنامه.
دو ویژگی یاد شده را میتوان بر روی پارامترهای متدها، پارامترهایی از نوع in و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
3) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی کار با نوعهای ارجاعی نال نپذیر
دو ویژگی جدید MaybeNull و NotNull کار پس بررسی نال پذیری را انجام میدهند:
در این مثال، متد Find با تعریف ویژگی return: MaybeNull، ممکن است نال برگرداند. برای مثال اگر چیزی یافت نشد، default را بر گرداند.
در متد Resize، پارامتر array، میتواند نال دریافت کند، چون نالپذیر تعریف شدهاست؛ اما چون به ویژگی NotNull مزین است، حاصل تغییرات بر روی آن (خروجی از متد، از طریق پارامتری از نوع ref) نمیتواند نال باشد.
دو ویژگی یاد شده را میتوان بر روی خروجی متدها، پارامترهایی از نوع out و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
4) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی «شرطی» کار با نوعهای ارجاعی نال نپذیر
در مثالهای زیر کاربردهای دو ویژگی شرطی جدید NotNullWhen و MaybeNullWhen را مشاهده میکنید:
در اینجا با بکارگیری ویژگی [NotNullWhen(false)] به فراخوان اعلام میکنیم که اگر IsNullOrEmpty مقدار false را برگرداند، مقدار value ارسال شدهی به آن، نال نیست.
در اینجا با بکارگیری ویژگی [NotNullWhen(true)] به فراخوان اعلام میکنیم که اگر TryParse مقدار true را برگرداند، مقدار version خروجی آن، نال نیست.
در اینجا با بکارگیری ویژگی [MaybeNullWhen(false)] به فراخوان اعلام میکنیم که اگر TryDequeue مقدار false را برگرداند، مقدار result خروجی آن، میتواند نال هم باشد.
5) اضافه شدن یک سری ویژگی توکار جدید برای شرط گذاشتن بین ورودی و خروجی، در حین کار با نوعهای ارجاعی نال نپذیر
در متد زیر، هم خروجی و هم ورودی آن میتوانند نال باشند. اما میخواهیم اگر path نال نباشد، اطمینان حاصل کنیم که استفاده کننده میداند، خروجی این متد، حتما نال نخواهد بود:
برای انجام یک چنین اطلاع رسانیهایی میتوان از ویژگی جدید NotNullIfNotNull استفاده کرد. از آن میتوان برای مزین سازی خروجی متدها و یا پارامترهایی از نوع ref استفاده کرد.
6) اضافه شدن یک سری ویژگی توکار جدید برای بررسی سیلان برنامه، در حین کار با نوعهای ارجاعی نال نپذیر
در اینجا نحوهی استفاده از دو ویژگی جدید DoesNotReturn و DoesNotReturnIf را مشاهده میکنید:
اگر متد ThrowArgumentNullException در جائی فراخوانی شود، سبب بروز یک استثناء میشود. استفاده از DoesNotReturn سبب میشود تا به کامپایلر اعلام کند، پس از این نقطه، دیگر نیازی به بررسی نال بودن اشیاء نیست؛ چون آن قطعه از کد، غیرقابل اجرا و رسیدن میشود. این ویژگی را تنها بر روی متدها میتوان قرار داد.
اگر متد MyAssert فراخوانی شود و ورودی آن false باشد، یک استثناء را صادر میکند. با بکارگیری ویژگی [DoesNotReturnIf(false)] این موضوع را به کامپایلر اعلام کرده و از آن درخواست میکنیم تا کار بررسی نال بودن اشیاء را از آن سطر به بعد، انجام ندهد. این ویژگی را تنها بر روی پارامترها میتوان اعمال کرد.
پس از نصب SDK جدید، نحوهی فعالسازی این قابلیت در فایل csproj، به صورت زیر درآمدهاست:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <LangVersion>8.0</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> </Project>
این موارد در Preview 7 جدید هستند:
1) امکان اضافه کردن قید جدید notnull در حین تعریف نوعهای جنریک
interface IDoStuff<TIn, TOut> where TIn : notnull where TOut : notnull { TOut DoStuff(TIn input); }
2) اضافه شدن یک سری ویژگی توکار جدید برای «پیش» بررسی کار با نوعهای ارجاعی نال نپذیر
ویژگی AllowNull به فراخوانها امکان ارسال نال را حتی اگر مجاز نباشد، میدهد. این ویژگی جدید در فضای نام System.Diagnostics.CodeAnalysis تعریف شدهاست. برعکس آن ویژگی DisallowNull، سبب خواهد شد تا فراخوانها حتی در صورت مجاز بودن نیز نتوانند نال ارسال کنند.
using System; using System.Diagnostics.CodeAnalysis; namespace ConsoleApp { public class MyClass { private string _innerValue = string.Empty; [AllowNull] public string MyValue { get { return _innerValue; } set { _innerValue = value ?? string.Empty; } } }
دو ویژگی یاد شده را میتوان بر روی پارامترهای متدها، پارامترهایی از نوع in و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
3) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی کار با نوعهای ارجاعی نال نپذیر
دو ویژگی جدید MaybeNull و NotNull کار پس بررسی نال پذیری را انجام میدهند:
public class MyArray { // Result is the default of T if no match is found [return: MaybeNull] public static T Find<T>(T[] array, Func<T, bool> match) { //... } // Never gives back a null when called public static void Resize<T>([NotNull] ref T[]? array, int newSize) { //... } }
در متد Resize، پارامتر array، میتواند نال دریافت کند، چون نالپذیر تعریف شدهاست؛ اما چون به ویژگی NotNull مزین است، حاصل تغییرات بر روی آن (خروجی از متد، از طریق پارامتری از نوع ref) نمیتواند نال باشد.
دو ویژگی یاد شده را میتوان بر روی خروجی متدها، پارامترهایی از نوع out و ref، فیلدها، خواص و ایندکسرها اعمال کرد.
4) اضافه شدن یک سری ویژگی توکار جدید برای «پس» بررسی «شرطی» کار با نوعهای ارجاعی نال نپذیر
در مثالهای زیر کاربردهای دو ویژگی شرطی جدید NotNullWhen و MaybeNullWhen را مشاهده میکنید:
public class MyString { // True when 'value' is null public static bool IsNullOrEmpty([NotNullWhen(false)] string? value) { //... } }
public class MyVersion { // If it parses successfully, the Version will not be null. public static bool TryParse(string? input, [NotNullWhen(true)] out Version? version) { //... } }
public class MyQueue<T> { // 'result' could be null if we couldn't Dequeue it. public bool TryDequeue([MaybeNullWhen(false)] out T result) { //... } }
5) اضافه شدن یک سری ویژگی توکار جدید برای شرط گذاشتن بین ورودی و خروجی، در حین کار با نوعهای ارجاعی نال نپذیر
در متد زیر، هم خروجی و هم ورودی آن میتوانند نال باشند. اما میخواهیم اگر path نال نباشد، اطمینان حاصل کنیم که استفاده کننده میداند، خروجی این متد، حتما نال نخواهد بود:
class MyPath { [return: NotNullIfNotNull("path")] public static string? GetFileName(string? path) { //... } }
6) اضافه شدن یک سری ویژگی توکار جدید برای بررسی سیلان برنامه، در حین کار با نوعهای ارجاعی نال نپذیر
در اینجا نحوهی استفاده از دو ویژگی جدید DoesNotReturn و DoesNotReturnIf را مشاهده میکنید:
internal static class ThrowHelper { [DoesNotReturn] public static void ThrowArgumentNullException(ExceptionArgument arg) { //... } }
public static class MyAssertionLibrary { public static void MyAssert([DoesNotReturnIf(false)] bool condition) { //... } }
نظرات مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
با توجه به عرضه نسخه net core.، این گزارشگر هم تغییراتی در نسخههای اخیر خود داده و هم اینکه پیشرفتهای زیادی نیز کرده است.
2- همچنین در نهایت باید فایل لایسنس نیز در پروژه قرار گرفته و در Startup به استیمول معرفی گرد. به همین جهت کلاس زیر را اضافه کنید:
که در خط دوم مسیر فایل لایسنس معرفی شده است که برای پروژه شما باید اصلاح شود و در startup.cs به صورت زیر استفاده میشود:
برای نمایش گزارش همه چیز کماکان مثل سابق است فقط یک سری جزئیات تغییر نموده است:
همچنین در ویو RequestReport.cshtml کد به شکل زیر تغییر میکند:
با توجه به نیاز کد بالا را تغییر دهید. تقریبا تا حدی خصوصیتها تغییر کردهاند ولی نه خیلی.
بعد از نصب نسخه مورد نظر (برای نسخه 3.1 نیاز به نصب استیمول 2020 است) طبق این لینک مشکل Trial بودن (برای ما) را حل کنید.
در مورد این لینک چند نکته قابل ذکر است:
1- از شما میخواهد که dllهای جایگزین بعد از build، با dllها اصلی جابجا شوند که انجام این مورد در Visual Studio به خاطر داشتن UI برای اینکار به راحتی انجام میشد ولی در Visual Studio Code و Rider، چنین موردی باید به صورت دستی صورت بگیرد؛ به همین علت در فایل csproj پروژه اصلی، کدهای زیر را اضافه کنید:
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command="xcopy /y /d $(ProjectDir)Packages\*.* $(OutDir)" /> </Target>
public static class StimulSoftLicense { public static void LoadLicense(IWebHostEnvironment environment) { var contentRoot = environment.ContentRootPath; var licenseFile = System.IO.Path.Combine(contentRoot,"Reports", "license.key"); Stimulsoft.Base.StiLicense.LoadFromFile(licenseFile); } }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { StimulSoftLicense.LoadLicense(env); }
public IActionResult RequestReport(int id) { return View(); } public IActionResult LoadReportData(int id) { StiReport report = new StiReport(); report.Load(StiNetCoreHelper.MapPath(this, "Reports/Requests/RequestInfo.mrt")); var landDetailsReport = GetOwnerReportData(id); report.RegBusinessObject("Land", landDetailsReport); return StiNetCoreViewer.GetReportResult(this, report); } public IActionResult ViewerEvent() { return StiNetCoreViewer.ViewerEventResult(this); }
<div style="direction: ltr"> @Html.StiNetCoreViewer(new StiNetCoreViewerOptions() { Theme = StiViewerTheme.Office2007Silver, Appearance = { RightToLeft = true, //ShowTooltips = false, ShowTooltipsHelp = false, }, Localization = "~/Reports/fa.xml", Actions = { GetReport = "LoadReportData", ViewerEvent = "ViewerEvent", }, Toolbar = { ShowAboutButton = false, ShowOpenButton = false, ShowSaveButton = true, ShowFindButton = false, ShowEditorButton = false, ShowDesignButton = false, ShowBookmarksButton = false, ShowResourcesButton = false, ShowParametersButton = false, ShowPinToolbarButton = false }, Exports = { DefaultSettings = { ExportToPdf = { CreatorString = "SANA" } }, ShowExportDialog = false, ShowExportToDocument = false, ShowExportToExcel = false, ShowExportToExcel2007 = false, ShowExportToHtml = false, ShowExportToHtml5 = false, ShowExportToImageBmp = false, ShowExportToImageJpeg = false, ShowExportToImagePcx = false, ShowExportToImagePng = false, ShowExportToImageMetafile = false, ShowExportToImageTiff = false, ShowExportToOpenDocumentCalc = false, ShowExportToMht = false, ShowExportToOpenDocumentWriter = false, ShowExportToXps = false, ShowExportToSylk = false, ShowExportToRtf = false, ShowExportToExcelXml = false, ShowExportToText = false, ShowExportToWord2007 = false, ShowExportToImageGif = false, ShowExportToCsv = false, ShowExportToDbf = false, ShowExportToDif = false, ShowExportToImageSvg = false, ShowExportToImageSvgz = false, ShowExportToPowerPoint = false, ShowExportToXml = false, ShowExportToJson = false } }) </div>
یک نکته مهم در مورد div که این helper را محصور کرده وجود دارد و آن این است که حالت RTL قالب شما روی Helper استیمول تاثیر میگذارد ( به خصوص روی خود گزارش) برای همین از تنظیمات داخل خود Helper برای اینکار استفاده میکنیم؛ ولی اگر قالب RTL باشد و استیمول هم روی RTL تنظیم شود در نوار ابزار، ترتیب ابزارها معکوس میگردد. به همین دلیل حالت پیش فرض، یعنی LTR میباشد و با تظیم Helper به این مهم دست پیدا میکنیم.
کدهای زیر را نیز به فایل css صفحه اضافه کنید تا در گزارش ساز استیمول اعمال گردد:
.stiJsViewerClearAllStyles { font-family: "IRANSans" !important; } //مخصوص دیالوگ تنظیمات خروجی در صورت فعال بودن .stiJsViewerGroupPanelContainer{ direction: rtl !important; } .stiJsViewerFormButtonDefault{ direction: rtl !important; } .stiJsViewerFormButtonOver{ direction: rtl !important; } .stiJsViewerFormButtonDefault>table>tbody>tr>td{ text-align: right !important; } .stiJsViewerFormButtonOver>table>tbody>tr>td{ text-align: right !important; } .stiJsViewerFormHeader{ direction: rtl; } .stiJsViewerFormHeader>table>tbody>tr>td{ text-align: right !important; } .stiJsViewerCheckBox{ direction:rtl !important; } .stiJsViewerDropdownPanel{ direction:rtl !important; } .stiJsViewerFormContainer td.stiJsViewerClearAllStyles{ direction:rtl !important; } .stiMvcViewerReportPanel table{ direction:ltr !important; }
فرم هایی که اطلاعاتی را از یک کاربر دریافت کرده و به سمت سرور Post میکنند، از مهمترین اجزای لاینفک یک وب سایت میباشند. بی شک همهی ما از چنین فرمهایی حتی در یک پروژهی هرچند کوچک استفاده کردهایم. ممکن است هنگام ارسال این فرمها، کاربری شیطنت به خرج داده و درون یکی از فیلدهای فرم، از عبارتهای HTML و یا یک اسکریپت استفاده کرده باشد. در ASP.Net + MVC از مکانیزم ValidationRequest برای مقابله با چنین حملاتی (XSS) استفاده شده است.
یک فرم با یک Textbox در یک صفحه قرار دهید و درون Textbox یک تگ HTML بنویسید و آنرا به سرور ارسال کنید، در حالت پیش فرض اتفاقی که خواهد افتاد اینست که با یک صفحهی خطا روبرو خواهید شد که به شما هشدار میدهد دادههای ارسال شده، دارای مقادیر غیرمجازی میباشند. شما میتوانید این مکانیزم اعتبارسنجی-امنیتی را غیرفعال کنید. برای غیرفعال کردن آن هم در لینکی که در فوق معرفی گردید توضیحات کافی داده شده است. ولی توصیه میشود برای جلوگیری از حملات XSS هیچگاه این مکانیزم را غیرفعال نکنید، مگر اینکه هیچ دادهای را بدون Encode کردن در صفحه نمایش ندهید.
راههای زیادی برای هندل کردن این خطا و مطلع کردن کاربر وجود دارند و معمولا از صفحهی خطای سفارشی برای اینکار استفاده میشود. اما ایدهی بهتری نیز برای مقابله با این خطا وجود دارد!
فرض کنید اطلاعات فرم شما از طریق Ajax به سرور ارسال میشود و نتیجه بصورت Json به مروگر برگشت داده میشود. در حالت معمول در صورت رخ دادن خطای فوق، سرور کد 500 را برگشت میدهد و تنها راه هندل کردن آن در رویداد error شئ Ajax شما میباشد؛ آنهم با یک پیغام ساده. ولی من ترجیح میدهم به جای صادر کردن خطای 500، در صورت وقوع این خطا آنرا بصورت یک خطای ModelState به کاربر نمایش دهم. به نظر من اینکار وجههی بهتری دارد و ضمنا لازم نیست در هر رویدادی این خطا را هندل کنید. برای رسیدن به این هدف هم چندین راه وجود دارد که سادهترین آنها اینست که یک ModelBinder سفارشی ایجاد کنید و با هندل کردن این خطا، یک پیام خطا را به ModelState اضافه کنید و بعد به MVC بگویید که از این ModelBinder برای پروژهی من استفاده کن. با این روش هرگاه کاربر دادهای حاوی کدهای HTML درون فیلدهای فرم وارد کرده باشد، بدون هیچ کار اضافهای وضعیت ModelState شما Invalid میشود و خطای مدل به کاربر نمایش داده خواهد شد.
ابتدا کلاسی برای ModelBinder سفارشی خود ایجاد کنید و از کلاس DefaultModelBinder ارث بری کنید. سپس با بازنویسی متد BindModel آن منطق خود را پیاده سازی کنید:
در این کلاس ابتدا سعی میکنیم بطور عادی کار را به متد BindModel کلاس پایه بسپاریم. اگر دادههای ارسال شده حاوی کد HTML نباشد، بدون هیچ خطایی Model Binding صورت میگیرد. ولی در صورتیکه کاربر در فیلدی، از کدهای HTML استفاده کرده باشد، یک خطای HttpRequestValidationException رخ خواهد داد که با هندل کردن آن هدف خود را تامین میکنیم.
برای اینکار در قسمت catch بلوک مدیریت خطا، ابتدا یک نمونه از کلاس ModelState را میسازیم. بعد پیام خطای مورد نظر خود را به Errorsهای آن اضافه میکنیم. حال باید یک زوج کلید/مقدار برای این ModelState تعریف کنیم و به bindingContext اضافه کنیم. کلید ما در اینجا نام مدل جاری و مقدار آن هم نام فیلدی از فرم است که سبب بروز این خطا شده است.
حالا نهایت کاری که باید انجام دهیم اینست که در رویداد Application_Start این مدل بایندیگ سفارشی را جایگزین مدل بایندیگ پیش فرض کنیم:
کار تمام است. از حالا به بعد در صورتیکه کاربر در فیلدهای هر فرمی از سایت شما از کدهای HTML استفاده کند دیگر خطایی رخ نمیدهد و فقط ModelState در وضعیت Invalid قرار میگیرد که میتوانید با قرار دادن یک ValidationMessage یا ValidationSummary به راحتی خطا را به کاربر نشان دهید.
یک فرم با یک Textbox در یک صفحه قرار دهید و درون Textbox یک تگ HTML بنویسید و آنرا به سرور ارسال کنید، در حالت پیش فرض اتفاقی که خواهد افتاد اینست که با یک صفحهی خطا روبرو خواهید شد که به شما هشدار میدهد دادههای ارسال شده، دارای مقادیر غیرمجازی میباشند. شما میتوانید این مکانیزم اعتبارسنجی-امنیتی را غیرفعال کنید. برای غیرفعال کردن آن هم در لینکی که در فوق معرفی گردید توضیحات کافی داده شده است. ولی توصیه میشود برای جلوگیری از حملات XSS هیچگاه این مکانیزم را غیرفعال نکنید، مگر اینکه هیچ دادهای را بدون Encode کردن در صفحه نمایش ندهید.
راههای زیادی برای هندل کردن این خطا و مطلع کردن کاربر وجود دارند و معمولا از صفحهی خطای سفارشی برای اینکار استفاده میشود. اما ایدهی بهتری نیز برای مقابله با این خطا وجود دارد!
فرض کنید اطلاعات فرم شما از طریق Ajax به سرور ارسال میشود و نتیجه بصورت Json به مروگر برگشت داده میشود. در حالت معمول در صورت رخ دادن خطای فوق، سرور کد 500 را برگشت میدهد و تنها راه هندل کردن آن در رویداد error شئ Ajax شما میباشد؛ آنهم با یک پیغام ساده. ولی من ترجیح میدهم به جای صادر کردن خطای 500، در صورت وقوع این خطا آنرا بصورت یک خطای ModelState به کاربر نمایش دهم. به نظر من اینکار وجههی بهتری دارد و ضمنا لازم نیست در هر رویدادی این خطا را هندل کنید. برای رسیدن به این هدف هم چندین راه وجود دارد که سادهترین آنها اینست که یک ModelBinder سفارشی ایجاد کنید و با هندل کردن این خطا، یک پیام خطا را به ModelState اضافه کنید و بعد به MVC بگویید که از این ModelBinder برای پروژهی من استفاده کن. با این روش هرگاه کاربر دادهای حاوی کدهای HTML درون فیلدهای فرم وارد کرده باشد، بدون هیچ کار اضافهای وضعیت ModelState شما Invalid میشود و خطای مدل به کاربر نمایش داده خواهد شد.
ابتدا کلاسی برای ModelBinder سفارشی خود ایجاد کنید و از کلاس DefaultModelBinder ارث بری کنید. سپس با بازنویسی متد BindModel آن منطق خود را پیاده سازی کنید:
using System.Web; using System.Web.Mvc; using System.Web.Helpers; using System.Globalization; namespace Parsnet { public class ParsnetModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { try { return base.BindModel(controllerContext, bindingContext); } catch (HttpRequestValidationException ex) { var modelState = new ModelState(); modelState.Errors.Add("اطلاعات ارسالی شما دارای کدهای HTML میباشند."); var key = bindingContext.ModelName; var value = controllerContext.RequestContext.HttpContext.Request.Unvalidated().Form[key]; modelState.Value = new ValueProviderResult(value, value, CultureInfo.InvariantCulture); bindingContext.ModelState.Add(key, modelState); } return null; } } }
در این کلاس ابتدا سعی میکنیم بطور عادی کار را به متد BindModel کلاس پایه بسپاریم. اگر دادههای ارسال شده حاوی کد HTML نباشد، بدون هیچ خطایی Model Binding صورت میگیرد. ولی در صورتیکه کاربر در فیلدی، از کدهای HTML استفاده کرده باشد، یک خطای HttpRequestValidationException رخ خواهد داد که با هندل کردن آن هدف خود را تامین میکنیم.
برای اینکار در قسمت catch بلوک مدیریت خطا، ابتدا یک نمونه از کلاس ModelState را میسازیم. بعد پیام خطای مورد نظر خود را به Errorsهای آن اضافه میکنیم. حال باید یک زوج کلید/مقدار برای این ModelState تعریف کنیم و به bindingContext اضافه کنیم. کلید ما در اینجا نام مدل جاری و مقدار آن هم نام فیلدی از فرم است که سبب بروز این خطا شده است.
حالا نهایت کاری که باید انجام دهیم اینست که در رویداد Application_Start این مدل بایندیگ سفارشی را جایگزین مدل بایندیگ پیش فرض کنیم:
ModelBinders.Binders.DefaultBinder = new ParsnetModelBinder();
کار تمام است. از حالا به بعد در صورتیکه کاربر در فیلدهای هر فرمی از سایت شما از کدهای HTML استفاده کند دیگر خطایی رخ نمیدهد و فقط ModelState در وضعیت Invalid قرار میگیرد که میتوانید با قرار دادن یک ValidationMessage یا ValidationSummary به راحتی خطا را به کاربر نشان دهید.
کارهای سورس باز قابل توجهی از برنامه نویسهای ایرانی یافت نمیشوند؛ عموما کارهای ارائه شده در حد یک سری مثال یا کتابخانههای کوچک است و در همین حد. یا گاهی هم انگشت شمار پروژههایی کامل. مثل یک وب سایت یا یک برنامه نصفه نیمه دبیرخانه و امثال آن. اینها هم خوب است از دیدگاه به اشتراک گذاری اطلاعات، ایدهها و هم ... یک مزیت دیگر را هم دارد و آن این است که بتوان کیفیت عمومی کد نویسی را حدس زد.
اگر کیفیت کدها رو بررسی کنید به یک نتیجهی کلی خواهید رسید: "عموم برنامه نویسهای ایرانی (حداقل اینهایی که چند عدد کار سورس باز به اشتراک گذاشتهاند) با مفهومی به نام Refactoring هیچگونه آشنایی ندارند". مثلا یک برنامهی WinForm تهیه کردهاند و کل سورس برنامه همان چند عدد فرم برنامه است و هر فرم بالای 3000 سطر کد دارد. دوستان عزیز! به این میگویند «فاجعهای به نام کدنویسی!» صاحب اول و آخر این نوع کدها خودتان هستید! شاید به همین جهت باشد که عمدهی پروژههای سورس باز پس از اینکه برنامه نویس اصلی از توسعهی آن دست میکشد، «میمیرند». چون کسی جرات نمیکند به این کدها دست بزند. مشخص نیست الان این قسمت را که تغییر دادم، کجای برنامه به هم ریخت. تستی ندارند. ساختاری را نمیتوان از آنها دریافت. منطق قسمتهای مختلف برنامه از هم جدا نشده است. برنامه یک فرم است با چند هزار سطر کد در یک فایل! کار شما شبیه به کد اسمبلی چند هزار سطری حاصل از decompile یک برنامه که نباید باشد!
به همین جهت قصد دارم یک سری «ساده» Refactoring را در این سایت ارائه دهم. روی سادگی هم تاکید کردم، چون اگر عموم برنامه نویسها با همین موارد به ظاهر ساده آشنایی داشتند، کیفیت کد نویسی بهتری را میشد در نتایج عمومی شده، شاهد بود.
این مورد در راستای نظر سنجی انجام شده هم هست؛ درخواست مقالات خالص سی شارپ در صدر آمار فعلی قرار دارد.
Refactoring چیست؟
Refactoring به معنای بهبود پیوسته کیفیت کدهای نوشته شده در طی زمان است؛ بدون ایجاد تغییری در عملکرد اصلی برنامه. به این ترتیب به کدهایی دست خواهیم یافت که قابلیت آزمون پذیری بهتری داشته، در مقابل تغییرات مقاوم و شکننده نیستند و همچنین امکان به اشتراک گذاری قسمتهایی از آنها در پروژههای دیگر نیز میسر میشود.
قسمت اول - مجموعهها را کپسوله کنید
برای مثال کلاسهای ساده زیر را در نظر بگیرید:
namespace Refactoring.Day1.EncapsulateCollection
{
public class OrderItem
{
public int Id { set; get; }
public string Name { set; get; }
public int Amount { set; get; }
}
}
using System.Collections.Generic;
namespace Refactoring.Day1.EncapsulateCollection
{
public class Orders
{
public List<OrderItem> OrderItems { set; get; }
}
}
نکته اول: هر کلاس باید در داخل یک فایل جدا قرار گیرد. «لطفا» یک فایل درست نکنید با 50 کلاس داخل آن. البته اگر باز هم یک فایل باشد که بتوان 50 کلاس را داخل آن مشاهده کرد که چقدر هم عالی! نه اینکه یک فایل باشد تا بعدا 50 کلاس را با Refactoring از داخل آن بیرون کشید!
قطعه کد فوق، یکی از روشهای مرسوم کد نویسی است. مجموعهای به صورت یک List عمومی در اختیار مصرف کننده قرار گرفته است. حال اجازه دهید تا با استفاده از برنامه FxCop برنامه فوق را آنالیز کنیم. یکی از خطاهایی را که نمایش خواهد داد عبارت زیر است:
Error, Certainty 95, for Do Not Expose Generic Lists
بله. لیستهای جنریک را نباید به همین شکل در اختیار مصرف کننده قرار داد؛ چون به این صورت هر کاری را میتوانند با آن انجام دهند، مثلا کل آن را تعویض کنند، بدون اینکه کلاس تعریف کننده آن از این تغییرات مطلع شود.
پیشنهاد FxCop این است که بجای List از Collection یا IList و موارد مشابه استفاده شود. اگر اینکار را انجام دهیم اینبار به خطای زیر خواهیم رسید:
Warning, Certainty 75, for Collection Properties Should Be ReadOnly
FxCop پیشنهاد میدهد که مجموعه تعریف شده باید فقط خواندنی باشد.
چکار باید کرد؟
بجای استفاده از List جهت ارائه مجموعهها، از IEnumerable استفاده کنید و اینبار متدهای Add و Remove اشیاء به آنرا به صورت دستی تعریف نمائید تا بتوان از تغییرات انجام شده بر روی مجموعه ارائه شده، در کلاس اصلی آن مطلع شد و امکان تعویض کلی آنرا از مصرف کننده گرفت. برای مثال:
using System.Linq;
using System.Collections.Generic;
namespace Refactoring.Day1.EncapsulateCollection
{
public class Orders
{
private int _orderTotal;
private List<OrderItem> _orderItems;
public IEnumerable<OrderItem> OrderItems
{
get { return _orderItems; }
}
public void AddOrderItem(OrderItem orderItem)
{
_orderTotal += orderItem.Amount;
_orderItems.Add(orderItem);
}
public void RemoveOrderItem(OrderItem orderItem)
{
var order = _orderItems.Find(o => o == orderItem);
if (order == null) return;
_orderTotal -= orderItem.Amount;
_orderItems.Remove(orderItem);
}
}
}
اکنون اگر برنامه را مجددا با fxCop آنالیز کنیم، دو خطای ذکر شده دیگر وجود نخواهند داشت. اگر این تغییرات صورت نمیگرفت، امکان داشتن فیلد _orderTotal غیر معتبری در کلاس Orders به شدت بالا میرفت. زیرا مصرف کننده مجموعه OrderItems میتوانست به سادگی آیتمی را به آن اضافه یا از آن حذف کند، بدون اینکه کلاس Orders از آن مطلع شود یا اینکه بتواند عکس العمل خاصی را بروز دهد.
قابلیت مقدار دهی اولیهی بانک اطلاعاتی (data seeding) توسط اجرای کدهای Migrations و متد DbMigrationConfiguration.Seed آن، در حین انتقال از EF 6x به EF Core ناپدید شده بود که مجددا با ارائهی EF Core 2.1 به نحو کاملا متفاوتی توسط یک Fluent API، در متد OnModelCreating قابل تعریف و استفادهاست.
کلاسهای موجودیتهای مثال جاری
برای توضیح قابلیت جدید مقدار دهی اولیهی بانک اطلاعاتی در +EF Core 2.1، از کلاسهای موجودیتهای ذیل استفاده خواهیم کرد:
روش مقدار دهی اولیهی تک موجودیتها
اکنون فرض کنید قصد داریم جدول مجلات را مقدار دهی اولیه کنیم. برای اینکار خواهیم داشت:
چند نکته در اینجا حائز اهمیت هستند:
- ذکر صریح مقدار Id یک رکورد (هرچند نوع Id آن auto-increment است).
- عدم ذکر مقدار Publisher.
اکنون اگر توسط دستورات Migrations مانند dotnet ef migrations add init، کار تولید کدهای متناظر به روز رسانی بانک اطلاعاتی را بر اساس این کدها تولید کنیم، در قسمتی از آن، یک چنین خروجی را دریافت خواهیم کرد:
در ادامه اگر از روی این کلاسهای مهاجرتها، اسکریپت معادل نهایی اعمالی به بانک اطلاعاتی را توسط دستور dotnet ef migrations script تولید کنیم، یک چنین خروجی حاصل میشود:
همانطور که مشاهده میکنید، اگر نوع بانک اطلاعاتی ما SQL Server باشد، ابتدا ثبت دستی فیلدهای IDENTITY تنظیم میشود و سپس Id رکورد جدید را بر اساس مقداری که مشخص کردهایم، درج میکند.
توسط متد HasData امکان درج چندین رکورد با هم نیز وجود دارد:
البته باید دقت داشت که متد HasData، برای کار با یک تک موجودیت، طراحی شدهاست و توسط آن نمیتوان در چندین جدول بانک اطلاعاتی، مقادیری را درج کرد.
در مورد دادههای نالنپذیر چطور؟
در مثال فوق اگر تنظیمات خاصیت Publisherای را که نال وارد کردیم، نالنپذیر تعریف کنیم:
و مجددا دستورات تولید کلاسهای Migrations را صادر کنیم، اینبار خطای واضح زیر حاصل خواهد شد:
همین پیام خطا با عدم ذکر صریح مقدار Id نیز تولید میشود. هرچند Id، یک فیلد auto-increment است، اما چون شرط IsRequired در مورد آن برقرار است، شامل بررسی فیلدهای نالنپذیر نیز میشود. به همین جهت ذکر آن در متد HasData اجباری است.
امکان استفادهی از Anonymous Types در متد HasData
فرض کنید برای کلاس موجودیت خود یک سازنده را نیز تعریف کردهاید:
چون در متد HasData ذکر Id موجودیت، اجباری است، دیگر نمیتوان یک چنین تعاریفی را ارائه داد:
برای رفع یک چنین مشکلاتی، امکان استفادهی از anonymous types نیز در متد HasData پیشبینی شدهاست. در این حالت میتوان بجای وهله سازی مستقیم شیء Magazine، یک anonymous type را وهله سازی کرد و در آن MagazineId را نیز ذکر کرد؛ بدون اینکه نگران این باشیم آیا این خاصیت عمومی است، خصوصی است و یا ... حتی تعریف شدهاست یا خیر!
که حاصل آن تولید یک چنین کد مهاجرتی است:
و سبب درج صحیح مقادیر فیلدهای یک رکورد جدول Magazines میشود.
حالت دیگر استفادهی از این قابلیت، کار با خواصی هستند که private set میباشند. فرض کنید کلاس موجودیت Magazine را به صورت زیر تغییر دادهاید:
که در آن Id اینبار از نوع Guid است و در سازندهی کلاس مقدار دهی میشود و همچنین خواص این موجودیت به صورت private set تعریف شدهاند. در این حالت اگر متد HasData این موجودیت را به صورت زیر تعریف کنیم:
هر بار که دستورات Migrations اجرا میشوند، یک Guid جدید به صورت خودکار ایجاد خواهد شد که سبب میشود، مقدار آغازین پیشین، از بانک اطلاعاتی حذف و مقدار جدید آن با یک Guid جدید، درج شود. به همین جهت نیاز است Guid را حتما به صورت دستی و مشخص، در متد HasData وارد کرد که چنین کاری با توجه به تعریف کلاس موجودیت فوق، مسیر نیست. بنابراین در اینجا نیز میتوان از یک anonymous type استفاده کرد:
مقدار دهی اولیهی اطلاعات به هم مرتبط
همانطور که پیشتر نیز ذکر شد، متد HasData تنها با یک تک موجودیت کار میکند و روش کار آن همانند کار با DbSetها نیست. به همین جهت نمیتوان اشیاء به هم مرتبط را توسط آن در بانک اطلاعاتی درج کرد. بنابراین برای درج اطلاعات یک مجله و مقالات مرتبط با آن، ابتدا باید مجله را ثبت کرد و سپس بر اساس Id آن مجله، کلید خارجی مقالات را به صورت جداگانهای مقدار دهی نمود:
پیشتر یک Magazine را با Id مساوی 1 ثبت کرده بودیم. اکنون این Id را در اینجا به صورت یک کلید خارجی، جهت درج یک مقالهی جدیدی استفاده میکنیم. حاصل آن یک چنین مهاجرتی است:
در اینجا چون PublishDate را ذکر نکردهایم (و DateTime نیز یک value type است)، کمترین مقدار ممکن را برای آن تنظیم کردهاست.
مقدار دهی اولیهی Owned Entities
complex types در EF 6x با مفهوم دیگری به نام owned types در EF Core جایگزین شدهاند:
در اینجا اطلاعات مربوط به Publisher، در طی یک عملیات Refactoring، تبدیل به یک کلاس مستقل شدهاند و سپس در تعریف کلاس موجودیت مجله، مورد استفاده قرار گرفتهاند. این کلاس جدید، دارای Id نیست.
متد HasData تنها اجازهی کار با یک نوع کلاس را میدهد. به همین جهت یکبار باید Magazine را بدون Publisher ثبت کرد. سپس در طی ثبتی دیگر میتوان نوع Publisher را توسط یک anonymous type متصل به Id مجلهی ثبت شده، درج کرد (متد OwnsOne کار ارتباط را برقرار میکند). علت استفادهی از anonymous type نیز درج Id ای است که در کلاس Publisher وجود خارجی ندارد.
این دو دستور، خروجی Migrations زیر را تولید میکنند:
محل صحیح اجرای Migrations در برنامههای ASP.NET Core 2x
زمانیکه متد ()context.Database.Migrate را اجرا میکنید، تمام مهاجرتهای اعمال نشده را به بانک اطلاعاتی اعمال میکند که این مورد شامل اجرای دستورات HasData نیز هست. روش فراخوانی این متد در ASP.NET Core 1x به صورت زیر در متد Configure کلاس Startup بود (و البته هنوز هم کار میکند):
متد applyPendingMigrations، کار وهله سازی IUnitOfWork را انجام میدهد. سپس متد Migrate آنرا اجرا میکند، تا تمام Migartions تولید شده، اما اعمال نشدهی به بانک اطلاعاتی به صورت خودکار به آن اعمال شوند. متد Migrate نیز به صورت زیر تعریف میشود:
روش بهتر اینکار در ASP.NET Core 2x، انتقال متد applyPendingMigrations به بالاترین سطح ممکن در برنامه، در فایل program.cs و پیش از اجرای متد Configure کلاس Startup است. به این ترتیب در برنامه، قسمتهایی که پیش از متد Configure شروع به کار میکنند و نیاز به دسترسی به بانک اطلاعاتی را دارند، با صدور پیام خطایی، سبب خاتمهی برنامه نخواهند شد:
کلاسهای موجودیتهای مثال جاری
برای توضیح قابلیت جدید مقدار دهی اولیهی بانک اطلاعاتی در +EF Core 2.1، از کلاسهای موجودیتهای ذیل استفاده خواهیم کرد:
public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public string Publisher { get; set; } public List<Article> Articles { get; set; } } public class Article { public int ArticleId { get; set; } public string Title { get; set; } public DateTime PublishDate { get; set; } public int MagazineId { get; set; } public Author Author { get; set; } public int? AuthorId { get; set; } } public class Author { public int AuthorId { get; set; } public string Name { get; set; } public List<Article> Articles { get; set; } }
روش مقدار دهی اولیهی تک موجودیتها
اکنون فرض کنید قصد داریم جدول مجلات را مقدار دهی اولیه کنیم. برای اینکار خواهیم داشت:
protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<Magazine>().HasData(new Magazine { MagazineId = 1, Name = "DNT Magazine" }); }
- ذکر صریح مقدار Id یک رکورد (هرچند نوع Id آن auto-increment است).
- عدم ذکر مقدار Publisher.
اکنون اگر توسط دستورات Migrations مانند dotnet ef migrations add init، کار تولید کدهای متناظر به روز رسانی بانک اطلاعاتی را بر اساس این کدها تولید کنیم، در قسمتی از آن، یک چنین خروجی را دریافت خواهیم کرد:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Magazine", null });
set IDENTITY_INSERT ON INSERT INTO "Magazines" ("MagazineId", "Name", "Publisher") VALUES (1, 'DNT Magazine', NULL);
توسط متد HasData امکان درج چندین رکورد با هم نیز وجود دارد:
modelBuilder.Entity<Magazine>() .HasData(new Magazine{ MagazineId=2, Name="This Mag" }, new Magazine{ MagazineId=3, Name="That Mag" } );
البته باید دقت داشت که متد HasData، برای کار با یک تک موجودیت، طراحی شدهاست و توسط آن نمیتوان در چندین جدول بانک اطلاعاتی، مقادیری را درج کرد.
در مورد دادههای نالنپذیر چطور؟
در مثال فوق اگر تنظیمات خاصیت Publisherای را که نال وارد کردیم، نالنپذیر تعریف کنیم:
modelBuilder.Entity<Magazine>().Property(m=>m.Publisher).IsRequired();
"The seed entity for entity type 'Magazine' cannot be added because there was no value provided for the required property 'Publisher'."
امکان استفادهی از Anonymous Types در متد HasData
فرض کنید برای کلاس موجودیت خود یک سازنده را نیز تعریف کردهاید:
public Magazine(string name, string publisher) { Name=name; Publisher=publisher; }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Magazine", "1105 Media"));
modelBuilder.Entity<Magazine>().HasData(new {MagazineId=1, Name="DNT Mag", Publisher="1105 Media"});
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Mag", "1105 Media" });
حالت دیگر استفادهی از این قابلیت، کار با خواصی هستند که private set میباشند. فرض کنید کلاس موجودیت Magazine را به صورت زیر تغییر دادهاید:
public class Magazine { public Magazine(string name, string publisher) { Name=name; Publisher=publisher; MagazineId=Guid.NewGuid(); } public Guid MagazineId { get; private set; } public string Name { get; private set; } public string Publisher { get; private set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Mag", "1105 Media");
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی اطلاعات به هم مرتبط
همانطور که پیشتر نیز ذکر شد، متد HasData تنها با یک تک موجودیت کار میکند و روش کار آن همانند کار با DbSetها نیست. به همین جهت نمیتوان اشیاء به هم مرتبط را توسط آن در بانک اطلاعاتی درج کرد. بنابراین برای درج اطلاعات یک مجله و مقالات مرتبط با آن، ابتدا باید مجله را ثبت کرد و سپس بر اساس Id آن مجله، کلید خارجی مقالات را به صورت جداگانهای مقدار دهی نمود:
modelBuilder.Entity<Article>().HasData(new Article { ArticleId = 1, MagazineId = 1, Title = "EF Core 2.1 Query Types"});
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی Owned Entities
complex types در EF 6x با مفهوم دیگری به نام owned types در EF Core جایگزین شدهاند:
public class Publisher { public string Name { get; set; } public int YearFounded { get; set; } } public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public Publisher Publisher { get; set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData (new Magazine { MagazineId = 1, Name = "DNT Magazine" }); modelBuilder.Entity<Magazine>().OwnsOne (m => m.Publisher) .HasData (new { Name = "1105 Media", YearFounded = 2006, MagazineId=1 });
این دو دستور، خروجی Migrations زیر را تولید میکنند:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher_Name", "Publisher_YearFounded" }, values: new object[] { 1, "DNT Magazine", "1105 Media", 2006 });
محل صحیح اجرای Migrations در برنامههای ASP.NET Core 2x
زمانیکه متد ()context.Database.Migrate را اجرا میکنید، تمام مهاجرتهای اعمال نشده را به بانک اطلاعاتی اعمال میکند که این مورد شامل اجرای دستورات HasData نیز هست. روش فراخوانی این متد در ASP.NET Core 1x به صورت زیر در متد Configure کلاس Startup بود (و البته هنوز هم کار میکند):
namespace EFCoreMultipleDb.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { applyPendingMigrations(app); // ... } private static void applyPendingMigrations(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetService<IUnitOfWork>(); uow.Migrate(); } } } }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContext : DbContext, IUnitOfWork { // ... public void Migrate() { this.Database.Migrate(); } } }
public static void Main(string[] args) { var host = BuildWebHost(args); using (var scope = host.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<yourDBContext>(); context.Database.Migrate(); } host.Run(); }
پیشنیاز:
- بررسی ویجت Kendo UI File Upload
در مطلب قبل جزئیات استفاده از ویجت آپلود فریمورک قدرتمند Kendo UI عنوان شدند. در این مطلب قصد داریم طریقهی استفاده از آن را به صورت پاپ آپ، در ویجت گرید Kendo بررسی کنیم.
مدل زیر را در نظر بگیرید:
و برای ایجاد یک دیتاسورس سمت کاربر برای گرید، یک منبع داده را با استفاده از مدل بالا تولید میکنیم:
سپس برای نمایش در گرید، سیم پیچیهای لازم را انجام میدهیم:
همانطور که میبینید در ستونهای گرید، یک ستون ImageName نیز اضافه شدهاست که بهصورت تمپلیت تصویر محصول را نمایش میدهد. برای خود فیلد نیز از یک تمپلیت ادیتور که در ادامه تعریف خواهیم کرد استفاده کردهایم:
برای آپلود فایل و حذف آن نیز اکشنهای لازم را در سمت سرور مینویسم:
حاصل کار بصورت تصویر زیر نمایش داده شده است:
- بررسی ویجت Kendo UI File Upload
در مطلب قبل جزئیات استفاده از ویجت آپلود فریمورک قدرتمند Kendo UI عنوان شدند. در این مطلب قصد داریم طریقهی استفاده از آن را به صورت پاپ آپ، در ویجت گرید Kendo بررسی کنیم.
مدل زیر را در نظر بگیرید:
var product = { ProductId: 1001, ProductName: "Product 1001", Available: true, Filename: "Image02.png" };
var productCount = 100; var products = []; function datasourceFilling() { for (var i = 0; i < productCount; i++) { var product = { ProductId: i, ProductName: "Product " + i + " Name", Available: i % 2 == 0 ? true: false, Filename: i % 2 == 0 ? "Image01.png" : "Image02.png" }; products.push(product); } }
function makekendoGrid() { $("#grid").kendoGrid({ dataSource: { data: products, schema: { model: { //id: "ProductId", fields: { ProductId: { editable: false, nullable: true }, ProductName: { validation: { required: true } }, Available: { type: "boolean" }, ImageName: { type: "string", editable: false }, Filename: { type: "string", validation: { required: true } } } } }, pageSize: 20 }, height: 550, scrollable: true, sortable: true, filterable: true, pageable: { input: true, numeric: false }, editable: { mode: "popup", }, columns: [ { field: "ProductName", title: "Product Name" }, { field: "Available", width: "100px", template: '<input type="checkbox" #= Available ? checked="checked" : "" # disabled="disabled" ></input>' }, { field: "ImageName", width: "150px", template: "<img src='/img/#=Filename#' alt='#=Filename #' Title='#=Filename #' height='80' width='80'/>" }, { field: "Filename", width: "100px", editor: fileEditor }, { command: ["edit"], title: " ", width: "200px" } ] }); var grid = $('#grid').data('kendoGrid'); grid.hideColumn(3); }
function fileEditor(container, options) { $('<input type="file" name="file"/>') .appendTo(container) .kendoUpload({ multiple: false, async: { saveUrl: "@Url.Action("Save", "Home")", removeUrl: "@Url.Action("Remove", "Home")", autoUpload: true, }, upload: function (e) { alert("upload"); e.data = { Id: options.model.Id }; }, success: function (e) { alert("success"); options.model.set("ImageName", e.response.ImageUrl); }, error: function (e) { alert("error"); alert("Failed to upload " + e.files.length + " files " + e.XMLHttpRequest.status + " " + e.XMLHttpRequest.responseText); } }); }
[System.Web.Mvc.HttpPost] public virtual ActionResult Save(HttpPostedFileBase file) { var exName = Path.GetExtension(file.FileName); var totalFileName = System.Guid.NewGuid().ToString().ToLower().Replace("-", "") + exName; var physicalPath = Path.Combine(Server.MapPath("/img"), totalFileName); file.SaveAs(physicalPath); return Json(new { ImageUrl = totalFileName }, "text/plain"); } [System.Web.Mvc.HttpPost] public virtual ContentResult Remove(string fileName) { if (fileName != null) { var physicalPath = Path.Combine(Server.MapPath("/img"), fileName); System.IO.File.Delete(physicalPath); } // Return an empty string to signify success return Content(""); }
حاصل کار بصورت تصویر زیر نمایش داده شده است: