نظرات اشتراک‌ها
راه حل مشکل خراب شدن فایل های cshtml
بله صد در صد ولی برای دو تا از همکارام پیش اومد که از ساعت اول روز کاری رو یه تسکی کار می‌کردن و برق‌ها میره و کارشون خراب میشه
تو شرکت هم از tfs استفاده می‌کردیم
وقتی از یه سورس کنترل استفاده می‌کنیم دم و دقیقه که نمی‌یاییم تغییرات مون رو ذخیره کنیم
مطالب
مدیریت بهتر کدهای تولید شده توسط WCF RIA Services

در حین کار با WCF RIA Services اگر تمام موجودیت‌های تعریف شده انتخاب شوند یک فایل طویل DomainService حاصل خواهد شد. کار کردن با این فایل نه زیبا است و نه ساده. بعد از یک مدت شاید بگوئیم، خوب! من به ازای هر جدول یک DomainService جدا تولید می‌کنم با نامی مختص به آن و این اطلاعات را در فایلی جداگانه نیز ذخیره خواهم کرد. پس از انجام اینکار با خطای زیر مواجه خواهیم شد:

The entity type ... is exposed by multiple DomainService types. Entity types cannot be shared across DomainServices

به صورت خلاصه: مهم نیست سیستم شما از چند جدول تشکیل شده است؛ مهم این است که تنها یک فایل DomainService را باید تولید کنید و البته این یک محدودیت نیست؛ یک هدف محسوب می‌شود؛ از این دیدگاه که موجودیت‌های مرتبط باید در یک Domain قرار گیرند و تنها در یک دومین هستند که روابط بین آن‌ها معنا پیدا می‌کند.
- روش توصیه شده برای مدیریت این کلاس DomainService طویل، استفاده از واژه‌ی کلیدی partial است (برای مثال public partial class MyDomainService). به این صورت هر موجودیت را می‌توان در یک فایل جداگانه قرار داد و به این ترتیب مدیریت ساده‌تری را بر روی اطلاعات تولید شده داشت و همچنین تمامی این فایل‌ها در نهایت یک کلاس واحد را تشکیل می‌دهند و اصل وجود یک DomainService واحد در برنامه زیر سؤال نخواهد رفت.
- کدهایی را که خودتان نیز به این مجموعه اضافه خواهید کرد، در لابلای کدهای تولید شده قرار ندهید. در صورت تغییری در جداول نیاز است تا این فایل‌ها مجددا تولید شوند و اینجا است که تمام تغییرات خود را از دست خواهید داد. برای این منظور باز هم یک کلاس partial دیگر را تعریف کنید تا کدهای سفارشی خود را بتوان به صورت مجزایی از کدهای تولید شده به صورت خودکار، در آن قرار داد (برای مثال به نام MyServiceNameDomainService.extensions.cs).
- جایی که قرار است یک سری از فایل‌ها مجددا تولید شوند استفاده از برنامه‌های سورس کنترل را فراموش نکنید؛ تا هر زمانی بتوان کدهای جدید را با کدهای نگارش‌های قبل به سادگی مقایسه کرد؛ یا حتی به نگارش‌های قبلی بازگشت نمود.
- این نکته را به خاطر داشته باشید که اگر اطلاعاتی به صورت خودکار از پیش تعریف شده و موجود است (کلاس دومین یا متادیتای آن)،‌ این اطلاعات حین استفاده از امکانات تولید کد خودکار، مجددا تولید نخواهند شد. به همین جهت باید ابتدا آن‌ها را به صورت comment در آورد (یا از پروژه خارج نمود؛ استفاده از امکانات include و exclude پوشه‌ها یا فایل‌ها در ویژوال استودیو) تا همواره کدهای آخرین اطلاعات موجود تولید گردند.

مطالب
فراخوانی GraphQL API در یک کلاینت Angular
در قسمت‌های قبل ( ^  ، ^ و  ^ ) 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
در ترمینال VS Code  دستور زیر را وارد نمایید: 
ng add apollo-angular
تنها کاری که بعد از اجرای دستور بالا نیاز است انجام دهیم، تنظیم آدرس سرور GraphQL می‌باشد. از این رو فایل graphql.module.ts را باز کنید و متغیر uri را مقدار دهی کنید.
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
کتابخانه apollo-client نیازمند AsyncIterable می‌باشد. به همین جهت مطمئن شوید فایل tsconfig.json شما به صورت زیر است:
{
  "compilerOptions": {
    // ...
    "lib": [
      "es2017",
      "dom",
      "esnext.asynciterable"
    ]
  }
}
اکنون شما تمام وابستگی‌های مورد نیاز را دارید. سپس می‌توانید اولین Apollo Client خود را اجرا کنید. 
در ادامه، فایل 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>
اکنون جهت اجرای پروژه، کد‌های ضمیمه شده در پایان قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را دانلود نمایید و آن را اجرا کنید: 
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();
  }
}
متد watchQuery، شیء QueryRef را برگشت می‌دهد که خصوصیت valueChanges را دارد که آن یک Observable می‌باشد. Observable تنها یک بار صادر خواهد شد و آن زمانی است که query کامل می‌شود و خصوصیت loading به false  تنظیم خواهد شد؛ مگر اینکه شما پارامتر notifyOnNetworkStatusChange را در watchQuery  به true  تنظیم کنید. شیء منتقل شده از طریق یک Observable، شامل خصوصیات loading ،error و data است. هنگامی که نتیجه query برگشت داده می‌شود، به خصوصیت data انتساب داده می‌شود. 
 


مابقی 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 }
    })
  }
اکنون شما می‌توانید با ویرایش کردن فایل app.component.ts یا ایجاد کردن کامپوننت‌های جدید، از نتایج بدست آمده استفاده کنید.


کد‌های کامل این قسمت را از ایجا دریافت کنید : GraphQL_Angular.zip
کد‌های کامل قسمت ( GraphQL Mutations در ASP.NET Core ( عملیات POST, PUT, DELETE ) ) را از اینجا دریافت کنید :  ASPCoreGraphQL_3.rar  
مطالب
استفاده از نگارش سوم Google Analytics API در سرویس‌های ویندوز یا برنامه‌های وب
در زمان نگارش این مطلب، آخرین نگارش API مخصوص Google Analytics، نگارش سوم آن است و ... کار کردن با آن دارای مراحل خاصی است که حتما باید رعایت شوند. در غیر اینصورت عملا در یک برنامه‌ی وب یا سرویس ویندوز قابل اجرا نخواهند بود. زیرا در حالت متداول کار با API مخصوص Google Analytics، ابتدا یک صفحه‌ی لاگین به Gmail باز می‌شود که باید به صورت اجباری، مراحل آن را انجام داد تا مشخصات تائید شده‌ی اکانت در حال استفاده‌ی از API، در پوشه‌ی AppData ویندوز برای استفاده‌های بعدی ذخیره شود. این مورد برای یک برنامه‌ی دسکتاپ معمولی مشکل ساز نیست؛ زیرا کاربر برنامه، به سادگی می‌تواند صفحه‌ی مرورگری را که باز شده‌است، دنبال کرده و به اکانت گوگل خود وارد شود. اما این مراحل را نمی‌توان در یک برنامه‌ی وب یا سرویس ویندوز پیگیری کرد، زیرا عموما امکان لاگین از راه دور به سرور و مدیریت صفحه‌ی لاگین به Gmail وجود ندارد یا بهتر است عنوان شود، بی‌معنا است. برای حل این مشکل، گوگل راه حل دیگری را تحت عنوان اکانت‌های سرویس، ارائه داده است که پس از ایجاد آن، یک یک فایل X509 Certificate برای اعتبارسنجی سرویس، در اختیار برنامه نویس قرار می‌گیرد تا بدون نیاز به لاگین دستی به Gmail، بتواند از API گوگل استفاده کند. در ادامه نحوه‌ی فعال سازی این قابلیت و استفاده از آن‌را بررسی خواهیم کرد.


ثبت برنامه‌ی خود در گوگل و انجام تنظیمات آن

اولین کاری که برای استفاده از نگارش سوم Google Analytics API باید صورت گیرد، ثبت برنامه‌ی خود در Google Developer Console است. برای این منظور ابتدا به آدرس ذیل وارد شوید:
سپس بر روی دکمه‌ی Create project کلیک کنید. نام دلخواهی را وارد کرده و در ادامه بر روی دکمه‌ی Create کلیک نمائید تا پروفایل این پروژه ایجاد شود.



تنها نکته‌ی مهم این قسمت، بخاطر سپردن نام پروژه است. زیرا از آن جهت اتصال به API گوگل استفاده خواهد شد.
پس از ایجاد پروژه، به صفحه‌ی آن وارد شوید و از منوی سمت چپ صفحه، گزینه‌ی Credentials را انتخاب کنید. در ادامه در صفحه‌ی باز شده، بر روی دکمه‌ی Create new client id کلیک نمائید.


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


در اینجا ابتدا یک فایل مجوز p12 را به صورت خودکار دریافت خواهید کرد و همچنین پس از ایجاد client id، نیاز است، ایمیل آن‌را جایی یادداشت نمائید:


از این ایمیل و همچنین فایل p12 ارائه شده، جهت لاگین به سرور استفاده خواهد شد.
همچنین نیاز است تا به برگه‌ی APIs پروژه‌ی ایجاد شده رجوع کرد و گزینه‌ی Analytics API آن‌را فعال نمود:


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


دادن دسترسی به Client ID ثبت شده در برنامه‌ی Google Analytics

پس از اینکه Client ID سرویس خود را ثبت کردید، نیاز است به اکانت Google Analytics خود وارد شوید. سپس در منوی آن، گزینه‌ی Admin را پیدا کرده و به آن قسمت، وارد شوید:


در ادامه به گزینه‌ی User management آن وارد شده و به ایمیل Client ID ایجاد شده در قسمت قبل، دسترسی خواندن و آنالیز را اعطاء کنید:


در صورت عدم رعایت این مساله، کلاینت API، قادر به دسترسی به Google Analytics نخواهد بود.


استفاده از نگارش سوم Google Analytics API در دات نت

قسمت مهم کار، تنظیمات فوق است که در صورت عدم رعایت آن‌ها، شاید نصف روزی را مشغول به دیباگ برنامه شوید. در ادامه نیاز است پیشنیازهای دسترسی به نگارش سوم Google Analytics API را نصب کنیم. برای این منظور، سه بسته‌ی نیوگت ذیل را توسط کنسول پاورشل نیوگت، به برنامه اضافه کنید:
 PM> Install-Package Google.Apis
PM> Install-Package Google.Apis.auth
PM> Install-Package Google.Apis.Analytics.v3
پس از نصب، کلاس GoogleAnalyticsApiV3 زیر، جزئیات دسترسی به Google Analytics API را کپسوله می‌کند:
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Google.Apis.Analytics.v3;
using Google.Apis.Analytics.v3.Data;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;

namespace GoogleAnalyticsAPIv3Tests
{
    public class AnalyticsQueryParameters
    {
        public DateTime Start { set; get; }
        public DateTime End { set; get; } 
        public string Dimensions { set; get; } 
        public string Filters { set; get; }
        public string Metrics { set; get; }
    }

    public class AnalyticsAuthentication
    {
        public Uri SiteUrl { set; get; }
        public string ApplicationName { set; get; }
        public string ServiceAccountEmail { set; get; }
        public string KeyFilePath { set; get; }
        public string KeyFilePassword { set; get; }

        public AnalyticsAuthentication()
        {
            KeyFilePassword = "notasecret";
        }
    }

    public class GoogleAnalyticsApiV3
    {
        public AnalyticsAuthentication Authentication { set; get; }
        public AnalyticsQueryParameters QueryParameters { set; get; }

        public GaData GetData()
        {
            var service = createAnalyticsService();
            var profile = getProfile(service);
            var query = service.Data.Ga.Get("ga:" + profile.Id,
                                QueryParameters.Start.ToString("yyyy-MM-dd"),
                                QueryParameters.End.ToString("yyyy-MM-dd"),
                                QueryParameters.Metrics);
            query.Dimensions = QueryParameters.Dimensions;
            query.Filters = QueryParameters.Filters;
            query.SamplingLevel = DataResource.GaResource.GetRequest.SamplingLevelEnum.HIGHERPRECISION;
            return query.Execute();
        }

        private AnalyticsService createAnalyticsService()
        {
            var certificate = new X509Certificate2(Authentication.KeyFilePath, Authentication.KeyFilePassword, X509KeyStorageFlags.Exportable);
            var credential = new ServiceAccountCredential(
                new ServiceAccountCredential.Initializer(Authentication.ServiceAccountEmail)
                {
                    Scopes = new[] { AnalyticsService.Scope.AnalyticsReadonly }
                }.FromCertificate(certificate));

            return new AnalyticsService(new BaseClientService.Initializer
            {
                HttpClientInitializer = credential,
                ApplicationName = Authentication.ApplicationName
            });
        }

        private Profile getProfile(AnalyticsService service)
        {
            var accountListRequest = service.Management.Accounts.List();
            var accountList = accountListRequest.Execute();
            var site = Authentication.SiteUrl.Host.ToLowerInvariant();
            var account = accountList.Items.FirstOrDefault(x => x.Name.ToLowerInvariant().Contains(site));
            var webPropertyListRequest = service.Management.Webproperties.List(account.Id);
            var webPropertyList = webPropertyListRequest.Execute();
            var sitePropertyList = webPropertyList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
            var profileListRequest = service.Management.Profiles.List(account.Id, sitePropertyList.Id);
            var profileList = profileListRequest.Execute();
            return profileList.Items.FirstOrDefault(a => a.Name.ToLowerInvariant().Contains(site));
        }
    }
}
در اینجا در ابتدا بر اساس فایل p12 ایی که از گوگل دریافت شد، یک X509Certificate2 ایجاد می‌شود. پسورد این فایل مساوی است با ثابت notasecret که در همان زمان تولید اکانت سرویس در گوگل، لحظه‌ای در صفحه نمایش داده خواهد شد. به کمک آن و همچنین ServiceAccountEmail ایمیلی که پیشتر به آن اشاره شد، می‌توان به AnalyticsService لاگین کرد. به این ترتیب به صورت خودکار می‌توان شماره پروفایل اکانت سایت خود را یافت و از آن در حین فراخوانی service.Data.Ga.Get استفاده کرد.


مثالی از نحوه استفاده از کلاس GoogleAnalyticsApiV3

در ادامه یک برنامه‌ی کنسول را ملاحظه می‌کنید که از کلاس GoogleAnalyticsApiV3 استفاده می‌کند:
using System;
using System.Collections.Generic;
using System.Linq;

namespace GoogleAnalyticsAPIv3Tests
{
    class Program
    {

        static void Main(string[] args)
        {
            var statistics = new GoogleAnalyticsApiV3
            {
                Authentication = new AnalyticsAuthentication
                {
                    ApplicationName = "My Project",
                    KeyFilePath = "811e1d9976cd516b55-privatekey.p12",
                    ServiceAccountEmail = "10152bng4j3mq@developer.gserviceaccount.com",
                    SiteUrl = new Uri("https://www.dntips.ir/")
                },
                QueryParameters = new AnalyticsQueryParameters
                {
                    Start = DateTime.Now.AddDays(-7),
                    End = DateTime.Now,
                    Dimensions = "ga:date",
                    Filters = null,
                    Metrics = "ga:users,ga:sessions,ga:pageviews"
                }
            }.GetData();


            foreach (var result in statistics.TotalsForAllResults)
            {
                Console.WriteLine(result.Key + " -> total:" + result.Value);
            }
            Console.WriteLine();

            foreach (var row in statistics.ColumnHeaders)
            {
                Console.Write(row.Name + "\t");
            }
            Console.WriteLine();

            foreach (var row in statistics.Rows)
            {
                var rowItems = (List<string>)row;
                Console.WriteLine(rowItems.Aggregate((s1, s2) => s1 + "\t" + s2));
            }

            Console.ReadLine();
        }
    }
}

چند نکته
ApplicationName همان نام پروژه‌ای است که ابتدای کار، در گوگل ایجاد کردیم.
KeyFilePath مسیر فایل مجوز p12 ایی است که گوگل در حین ایجاد اکانت سرویس، در اختیار ما قرار می‌دهد.
ServiceAccountEmail آدرس ایمیل اکانت سرویس است که در قسمت ادمین Google Analytics به آن دسترسی دادیم.
SiteUrl آدرس سایت شما است که هم اکنون در Google Analytics دارای یک اکانت و پروفایل ثبت شده‌است.
توسط AnalyticsQueryParameters می‌توان نحوه‌ی کوئری گرفتن از Google Analytics را مشخص کرد. تاریخ شروع و پایان گزارش گیری در آن مشخص هستند. در مورد پارامترهایی مانند Dimensions و Metrics بهتر است به مرجع کامل آن در گوگل مراجعه نمائید:
Dimensions & Metrics Reference

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


برای مطالعه بیشتر
Using Google APIs in Windows Store Apps
How To Use Google Analytics From C# With OAuth
Google Analytic’s API v3 with C#
.NET Library for Accessing and Querying Google Analytics V3 via Service Account
Google OAuth2 C#
مطالب
xamarin.android قسمت سوم
Theme
برای اینکه بتوانیم ظاهر گرافیکی layout‌ها را کنترل نماییم، از Theme که مجموعه‌ای از styleهای گرافیکی می‌باشد، استفاده می‌کنیم. در اندروید مجموعه‌ای از تم‌های از پیش ساخته شده که به آنها Builtin Theme نیز گفته می‌شود می‌توانیم استفاده کنیم. تم‌ها ظاهر گرافیکی کلیه کنترل‌های Layout را با نام‌های زیر، کنترل می‌کنند:
statusBarColor
textColorPrimary
colorAccent
ColorPrimary
WindowBackground

اگر ساختار زیر را در یک صفحه استاندارد برنامه‌های موبایل را در نظر بگیریم، styleها هر بخش، یک نام منحصر به فرد دارد:



اگر بخواهیم از style‌های از پیش طراحی شده‌ی اندروید استفاده نماییم، ابتدا میتوانیم در صفحه‌ی layout در زامارین، style مربوطه را از بخش Theme استفاده کرده و نتیجه را مشاهده کنیم. ولی تغییر style سبب تغییر layout در زمان اجرا نمی‌شود. هرگاه بخواهیم از styleهای استاندارد یا builtin اندروید استفاده نماییم، در Activity توسط خصوصیت Theme با فرمت:
[Activity(Theme = "@style/NameThem")]
تم را به‌عنوان تم داخلی وسپس نام کامل تم را می‌نویسیم.
 
CustomTheme
در طراحی فرم‌ها ممکن است بخواهیم از یک استایل خاص builtin استفاده کنیم؛ ولی ممکن است بعضی از استایل‌های آن را استفاده نکنیم، مانند تمی که از قبل استفاده شده‌است، از روش زیر استفاده می‌کنیم:
- بر روی دایرکتوری value راست کلیک میکنیم. گزینه add new item را انتخاب و یک فایل xmlfile را با نام style ایجاد میکنیم.
- style‌های جدید منابع application می‌باشند که در بخش resource از آن‌ها استفاده میکنیم. هر استایل جدید را توسط Style Tag مشخص میکنیم و در خصوصیت Name، نام Style را مشخص میکنیم.
ممکن است در یک Style نتوانیم و یا نخواهیم تمامی Style‌های مورد نیاز را تامین کنیم. از این رو توسط Parent، یک StyleBuition تعریف نموده که این Styleها از آن مشتق می‌شوند. اگر در Theme جدید گزینه‌ها مشخص شوند، تم اصلی تغییر نمی‌کند. در غیر اینصورت تمامی گزینه‌های تعریف شده در تم جدید از ThemParent مقدار دهی می‌شود.
توسط item می‌توان یک style را تعریف نمود. در Activity توسط
 [Activity(Theme = "@style/NameThem")]
می‌توانیم استایل سفارشی شده خودمان را مشخص کنیم.

<?xml version="1.0" encoding="utf-8" ?>
<resources>
  <style name="MainTheme" parent="Theme.AppCompat.Light.DarkActionBar">
     <item name="windowNoTitle">true</item>
     <item name="windowActionBar">false</item>
  </style>
</resources>
 
  
Navigation Menu
کتابخانه متریال دیزاین کتابخانه جدیدی می‌باشد که به اندروید اضافه شده‌است. توسط  آن می‌توانیم کنترل‌های جدید را با استایل‌های جدید برای Appهای اندروید، تولید کنیم. ابتدا نیاز به نصب Component‌های ذیل در زامارین می‌باشد.
 
استفاده که از کتابخانه متریال دیزاین
برای اینکه بتوانیم Navigation menu را ایجاد کنیم، باید از نیوگت، کتابخانه‌های Appcompat و Designlibrary را انتخاب و نصب نماییم و اگر نگارش ویژوال استودیوی شما 15.7.3باشد، از ابتدا بصورت اتوماتیک نصب شده است و احتیاجی به این مراحل نمی‌باشد. بدیهی است در زمان نصب باید از نرم افزار‌های تحریم گذر نیز استفاده کرد.
  
ساخت Menu
NavigationMenu، منوی اصلی منو می‌باشد که با Swipping از گوشه راست به چپ، باز یا بسته می‌شود یا با کلیک بر روی دکمه‌ی Menu بر روی Toolbar، منو را باز یا بسته میکند.
قدم اول: نصب منو
منظور همان اضافه کردن کتابخانه‌ها است.
قدم دوم:
ابتدا باید گزینه‌های منو را در یک فایل xml تعریف نمود. هر گزینه‌ی آن از دو بخش متن اصلی منو و ID منو تشکیل شده‌است.
بر روی دایرکتوری Resource راست کلیک کرده و یک دایرکتوری یا پوشه را به نام Menu ایجاد می‌کنیم. بر روی دایرکتوری منو، راست کلیک کرده و یک فایل Xml را به آن اضافه می‌کنیم. برای آنکه بتوانیم در این فایل دستورات ساخت منو را نوشته و به نحوی که توسط اندروید قابل خواندن و تبدیل به منو باشد، ساختار منو را از آدرس زیر ویژوال استودیو دانلود میکنیم.
 <menu xmlns:android="http://schemas.android.com/apk/res/android">

توسط Group مجموعه گزینه‌های منو را را معرفی میکنیم.
هر گزینه در منو ی اصلی توسط یک آیتم مشخص میشود. برای آنکه هنگام کلیک بر روی هر گزینه از طریق برنامه نویسی بتوانیم گزینه انتخاب شده را شناسایی کنیم، یک آی دی منحصر بفرد را به هر گزینه اختصاص میدهیم. زمانیکه بر روی یک گزینه کلیک میشود، توسط این ID‌ها میتوانیم شناسایی کنیم کدام گزینه انتخاب شده‌است.
خصوصیت Android :Title متن اصلی منو را مشخص میکند.
 <?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <group android:checkableBehavior="single">
     <item android:id="@+id/menuItemHome" android:title="صفحه اصلی"></item>
     <item android:id="@+id/menuItemInsertProduct" android:title="ورود کالا جدید" ></item>
     <item android:id="@+id/menuItemListProduct" android:title="مشاهده کالاها"></item>
     <item android:id="@+id/menuItemExit"  android:title="خروج"></item>
  </group>
</menu>

سپس باید در Layout مورد نظر همانند صفحه Main، ساختار اصلی برنامه شامل Toolbar و Menu را بصورت زیر تعریف نماییم:
<android.support.v7.widget.Toolbar
  android:layout_width="match_parent"
  android:id="@+id/toolbar1"
  android:background="#33B86C"
  android:minHeight="?android:attr/actionBarSize"
  android:layout_height="wrap_content">
</android.support.v7.widget.Toolbar>

ساختار منو به صورت زیر است:
<?xml version="1.0" encoding="utf-8" ?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
  <group android:checkableBehavior="single">
    <item android:id="@+id/menuItemHome" android:title="صفحه اصلی"></item>
    <item android:id="@+id/menuItemInsertProduct" android:title="ورود کالا جدید" ></item>
    <item android:id="@+id/menuItemListProduct" android:title="مشاهده کالاها"></item>
    <item android:id="@+id/menuItemExit"  android:title="خروج"></item>
  </group>
</menu>
در Linearlayout ریشه، گزینه Fitssystemwindow را true میکنیم که سایز linearlayout را با سایز موبایل جدید اندازه می‌کند. سپس از toolbox، کنترل Toolbarرا به پنجره اضافه میکنیم که در بالای صفحه قرار می‌گیرد.
toolbar اضافه شده، toolbar استاندارد قبل از متریال دیزاین میباشد. در واقع toolbar اول، Toolbar استاندارد اندروید می‌باشد. برای آنکه از Toolbar متریال دیزاین استفاده کنیم، کنترل‌های متریال دیزاین در بخش supportlibrary اضافه میشود و Toolbar متریال دیزاین را اضافه میکنیم. علامت ؟ یعنی اینکه می‌خواهیم از اندازه سیستمی استفاده کنیم. اگر بخواهیم حداقل سایز Toolbarبر اساس پیش فرض در دستگاه‌های مختلف باشد، از علامت Android :attr ? استفاده میکنیم. اگر بخواهیم حداقل ارتفاع پیشنهادی اندروید در هر موبایل متصل شود، از خصوصیت Action Bar Size  استفاده میکنیم. این خصوصیت زمانی عمل میکند که Height  آن Wrapcontent باشد.
گذاشتن دکمه منو: برای آنکه بتوانیم دکمه منو را به Toolbar اضافه کنیم، از دکمه Image Button استفاده میکنیم که یک دکمه‌ی معمولی می‌باشد ولی خلاصه‌ی آن عکس است. در خصوصیت Back ground دکمه، بصورت زیر نام فایل آیکن منو را در دایرکتوری Drawable، مشخص میکنیم و خصوصیت src آن‌را null می‌کنیم تا تصویری بجز تصویر انتخابی نباشد.
برای آنکه بتوانیم پنجره اصلی منو را به صورتیکه دارای قابلیت حرکت به راست و چپ باشد، ایجاد کنیم، از کنترلی به‌نام  Drowerlayout استفاده میکنیم که بر روی صفحه قرار میگیرد. DrawerLayout در linearlayout ریشه قرار میگیرد و یا بعد از ToolBar و حتما باید خصوصیت fitsystemwindow کنترل Drawer را True کنیم. جهت نمایش گزینه‌های اصلی در Drawer از کنترل NavigationٰView استفاده می‌کنیم.
گزینه‌های منو در کنترلی به نام Navigationview قرار دارد. این کنترل باید در Drawerlayout قرار گیرد. توسط فضای نام منو، محل فایل xml را که منو درون آن قرار گرفته است، مشخص می‌کنیم. آدرس این دستور در این مسیر می‌باشد:
 xmlns:app="http://schemas.android.com/apk/res-auto"
Layout gravity  آن را end  قرار میدهیم که از سمت راست قرار بگیرد. Fit system Window را هم True میکنیم تا گزینه‌های داخل آن‌را هم Fit کند. Theme باید از نوع تم‌های متریال دیزاین و با کلمه Them . App Compact. ligth.NoActionBar باشد. برای آنکه اکتیویتی‌ها، متریال دیزاین را ساپورت کنند، میتوان از کلاس  App compact Activity  استفاده کنیم. Tool bar بصورت پیش فرض لیبل اکتیویتی را نشان می‌دهد و دستور زیر عنوان Toolbar را حذف میکند.
 SupportActionBar.SetDisplayShowTitleEnabled(false);
 
مدیریت گزینه‌های منو
به محض انتخاب یک گزینه درون NavigationView، رخدادی به نام NavigationItemSelected صادر می‌شود که توسط آن میتوانیم گزینه‌ی انتخاب شده را از طریق برنامه نویسی مدیریت کنیم. این کنترل در Android.Support.V7.Widget و NameSpace بالا قرار میگیرد. سپس یک رخ‌داد گردان را با نام navigationItemSelected پیاده سازی می‌کنیم. اطلاعات مربوط به گزینه‌ی انتخاب شده، در پارامتر دوم از این تابع NavigationView.NavigationItemSelectedEventArgs ذخیره می‌شود. ID، آیتم انتخاب شده در فایل Menu را باز می‌گرداند.
        var navigationview = this.FindViewById<NavigationView>(Resource.Id.navigationView1);
        navigationview.NavigationItemSelected += Navigationview_NavigationItemSelected;
        private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e)
        {
            Intent intent = null;
            switch (e.MenuItem.ItemId)
            {
                case Resource.Id.menuItemHome:

                    break;
                case Resource.Id.menuItemExit:
                    Finish();
                    break;
                case Resource.Id.menuItemInsertProduct:

                    break;
                case Resource.Id.menuItemListProduct:

                    break;
            }
        }
 
مدیریت اکتیویتی‌ها توسط Menu
با انتخاب گزینه Menu باید اکتیویتی مربوطه انتخاب شود. بنابراین برای هر گزینه‌ی منو یک Layout و اکتیویتی را ایجاد می‌کنیم و اجرا میکنیم. ولی در اکتیویتی جدید Toolbar وجود ندارد.
 
تکنیک ادغام:
برای آنکه در Layoutهای مختلف، تولبار و منو و یا هر View دیگری را بصورت مشترک استفاده کنیم، یک فایل xml را به دایرکتوری Layout اضافه می‌کنیم. دستور Merge میتواند تمام layoutها را به درون layoutهای دیگر مانند home,insert ادغام و یا تزریق کند. جهت استفاده از Merge در layoutهای دیگر نیاز به Id منحصر به فرد می‌باشد.
<?xml version="1.0" encoding="utf-8" ?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" 
    xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/toolbarlayout">
    <android.support.v7.widget.Toolbar android:layout_width="match_parent" android:id="@+id/toolbar1" android:background="#33B86C" android:minHeight="?android:attr/actionBarSize" android:layout_height="wrap_content">
        <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/imageButton1" android:background="@drawable/mainmenu" android:layout_gravity="end" />
    </android.support.v7.widget.Toolbar>
    <android.support.v4.widget.DrawerLayout android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/drawerLayout1" android:fitsSystemWindows="true">
        <android.support.design.widget.NavigationView android:minWidth="25px" android:minHeight="25px" android:layout_width="200dp" android:layout_height="match_parent" android:layout_gravity="end" app:menu="@menu/menu" android:id="@+id/navigationView1" android:fitsSystemWindows="true" />
    </android.support.v4.widget.DrawerLayout>
</merge>

در اکتیویتی‌های دیگر باید Toolbar و مدیریت گزینه‌های منو با کد‌های مشابه Main انجام شود.
        private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e)
        {
            Intent intent = null;
            switch (e.MenuItem.ItemId)
            {
                case Resource.Id.menuItemHome:
                    intent = new Intent(this, typeof(MainActivity));
                    break;
                case Resource.Id.menuItemExit:
                    Finish();
                    break;
                case Resource.Id.menuItemInsertProduct:
                    intent = new Intent(this, typeof(InsertActivity));
                    break;
                case Resource.Id.menuItemListProduct:
                    intent = new Intent(this, typeof(ListProductsActivity));
                    break;
            }
            if (intent != null) { }
        }
بنابراین دستورات xmlTollbar  و darawer layout در تمامی Layoutها و دستورات سی شارپ، کنترل کننده Toolbar و منو در تمامی اکتیویتی‌ها تکرار شده‌اند.
 
حل مشکلات Layout
یک فایل Xml را به Layout  اضافه می‌کنیم و درون آن Tag merge و کد‌های مشترک Drawer out و Toolbar را داخل تگ Merge اضافه می‌کنیم. جهت استفاده از کدهای (مقدار فایل ایکس ام ال ساخته شده که Tag merge داخل آن است)  Merge، در layout های دیگر، از دستور Include  استفاده می‌کنیم.
نام لی‌آوت را در خصوصیت Layout اضافه می‌کنیم. برای آنکه کد‌های سی شارپ کنترل کننده‌ی Toolbar و Menu چندین Toolbar وجود دارد که در یکی از آن‌ها یک کلاس واسط از کلاس app compat Activity  را به ارث میبریم. تابع Protected را از آن بازنویسی کرده و تمام کد‌های مدیریت Toolbar و منو را در آن پیاده سازی می‌کنیم. تمام اکتیویتی‌های برنامه را از این کلاس به ارث می‌بریم. بنابراین تابع InitieToolbar به تمامی فرزندان نیز به ارث برده می‌شود. در زمان اجرای دستورات، this ، اکتیویتی جاری می‌باشد.
    public class BaseAcitivity : AppCompatActivity
    {
        protected void InitieToolbar()
        {
            var toolbar = this.FindViewById<widgetV7.Toolbar>(Resource.Id.toolbar1);
            this.SetSupportActionBar(toolbar);
            //SupportActionBar.SetDisplayShowTitleEnabled(false);
            var imagebutton = toolbar.FindViewById<ImageButton>(Resource.Id.imageButton1);
            imagebutton.Click += Imagebutton_Click;
            var navigationview = this.FindViewById<NavigationView>(Resource.Id.navigationView1);
            navigationview.NavigationItemSelected += Navigationview_NavigationItemSelected;
        }

        private void Navigationview_NavigationItemSelected(object sender, NavigationView.NavigationItemSelectedEventArgs e)
        {
            Intent intent = null;
            switch (e.MenuItem.ItemId)
            {
                case Resource.Id.menuItemHome:
                    intent = new Intent(this, typeof(MainActivity));
                    break;
                case Resource.Id.menuItemExit:
                    Finish();
                    break;
                case Resource.Id.menuItemInsertProduct:
                    intent = new Intent(this, typeof(InsertActivity));
                    break;
                case Resource.Id.menuItemListProduct:
                    intent = new Intent(this, typeof(ListProductsActivity));
                    break;
            }
            if (intent != null)
                StartActivity(intent);
        }

        private void Imagebutton_Click(object sender, EventArgs e)
        {
            var drawerlayout = this.FindViewById<DrawerLayout>(Resource.Id.drawerLayout1);
            if (drawerlayout.IsDrawerOpen(Android.Support.V4.View.GravityCompat.End) == false)
            {
                drawerlayout.OpenDrawer(Android.Support.V4.View.GravityCompat.End);
            }
            else
            {
                drawerlayout.CloseDrawer(Android.Support.V4.View.GravityCompat.End);
            }
        }
    }
 
اگر بخواهیم یک تم در تمامی اکتیویتی‌ها  به صورت سراسری استفاده شود، از فایل تنظمیات اندروید بنام AndroidManifest در دایرکتوری Properties استفاده می‌کنیم و در  بخش Application Theme، نام تم را مشخص میکنیم:
 android:theme="@style/Theme.AppCompat.Light.NoActionBar"
 

ساخت TabPage
پیشنیاز: نصب کتابخانه‌های متریال دیزاین همانند قبل و طبق ورژن Sdk نصب شده
اگر بخواهیم چندین صفحه را بر روی یکدیگر Stack و یا Overload نماییم، از Tabpage استفاده می‌کنیم. صفحاتی‌که از TabPage استفاده می‌کنند، با انگشت جابجا میشوند و همانند برنامه‌ی واتساپ Fragment می‌باشند و هر Fragment دارای layout و اکتیویتی مربوط به خود می‌باشد. معماری layout آن بصورت زیر است:


ToolBar، در بالای فرم قرار می‌گیرد. TabLayou که بصورت TabPage آن‌ها را به عهده دارد. Viewpager مدیریت Layout‌ها را به هنگام Swipe یا جابجایی به عهده دارد.
یک layout را برای Toolbar قرار می‌دهیم. سپس Layout اصلی main را طراحی میکنیم. پس از اضافه کردن ToolBar، ابزار TabLayout را در بخش SupportLibrary متریال دیزاین انتخاب و در صفحه می‌کشیم. TabLayout در پایین Toolbar قرار می‌گیرد و با انتخاب رنگ یکسان برای هر دو، متصل و یکنواخت به نظر می‌رسد. سپس از Layout از Toolbar آیتم ViewPager را بر روی صفحه قرار می‌دهیم. اگر LayoutWeight آن را یک قرار دهیم، تمام ارتفاع صفحه را به ما تخصیص می‌دهد. زمانیکه در TabLayout تب‌ها جابجا می‌شوند یا بر روی یک آیکن کلیک می‌شود، صفحه مربوطه در بخش ViewPager به کاربر نمایش داده میشود. هر Page یک فرگمنت می‌باشد. به ازای هر فرگمنت یک Layout به دایرکتوری layout اضافه کرده و به ازای هر layoutFragment یک Activity Fragment را اضافه می‌کنیم. یک اکتیویتی از نوع را Android.Support.v4.AppFragment ایجاد میکنیم.
    public class Fragment1 : Android.Support.V4.App.Fragment
    {
        public override void OnCreate(Bundle savedInstanceState)
        {
            base.OnCreate(savedInstanceState);
        }
        public override View OnCreateView(LayoutInflater inflater,
      ViewGroup container, Bundle savedInstanceState)
        {
            return inflater.Inflate(Resource.Layout.FragmentLayout1, container, false);
        }
    }
ابتدا باید viewpager در Layout اصلی را پیدا کرده و با دستور زیر به Tablayout متصل کنیم:
 var tablayout = FindViewById<Android.Support.Design.Widget.TabLayout>(Resource.Id.tabLayout1);
var viewpager = FindViewById<ViewPager>(Resource.Id.viewPager1);
tablayout.SetupWithViewPager(viewpager);
زمانیکه آیکن را در TabLayout انتخاب میکنیم یا با انگشت Swipe میکنیم، به ترتیب بین صفحات که از Position صفحه آغاز شده‌اند، حرکت می‌کنیم، باید فرگمنت همان Position را نشان دهیم و این مدیریت توسط بخشی به‌نام Adapter انجام میشود. یک Adapter را به کنترلر اضافه میکنیم و از کلاس Fragment pager adapter به ارث می‌بریم. بر روی کلاس Fragment pager adapter ، دکمه‌های کنترل و نقطه را میزنیم و سپس کلاس را پیاده سازی می‌کنیم. در این حالت دو تابع را به ما می‌دهد: تابع Get item .count مجددا بر روی کلاس پدر راست کلیک میکنیم. در تابع کانت تعداد کل صفحه‌ها را (Layout ها) را انتخاب میکنیم. هرگاه از یک صفحه به صفحه‌ی دیگری انتقال پیدا کنیم، موقعیت صفحه جدیدی که از یک شروع میشود را به تابع Get Item بر اساس موقعیت Object  از fragment مربوطه new کرده و بعنوان خروجی باز می‌گرداند.
    class TabFragmentAdapter : FragmentPagerAdapter
    {
        public TabFragmentAdapter(FragmentManager fm) : base(fm)
        {
        }
        public override int Count => 3;
        public override Fragment GetItem(int position)
        {
            switch (position)
            {
                case 0: return new Fragment1();
                case 1: return new Fragment2();
                case 2: return new Fragment3();
                default: return new Fragment1();
            }
        }


        //int f1() { return 100; }
        //int f1 => 100;
    }
و در اکتیویتی اصلی، کد زیر را برای Load فرگمنت‌ها نیز قرار می‌دهیم:
 viewpager.Adapter = new TabFragmentAdapter(this.SupportFragmentManager);
  
آیکن برای TabPage
سپس اگر بخواهیم آیکن‌های Tab را به ترتیب تعریف کنیم، از تابع Gettabat استفاده میکنیم. پارامتر ورودی آن موقعیت Tab page میباشد و Set icon هم آیکن‌های دایرکتوری Drawable را انتخاب میکند.
 tablayout.GetTabAt(0).SetIcon(Resource.Drawable.iconCall);
 

نمایش متن همراه با عکس
 اگر بخواهیم آیکن‌های تب پیج را سفارشی کنیم، از Layout استفاده میکنیم که عرض و ارتفاع آن wrap Content  باشند و درون آن یک Text view که معادل Lable میباشند، قرار میدهند:
 View iconlayout1 = LayoutInflater.Inflate(Resource.Layout.custom_TabIconLayout, null);
var txt = iconlayout1.FindViewById<TextView>(Resource.Id.tabTextIcon);
txt.Text = "تماس";
txt.SetCompoundDrawablesWithIntrinsicBounds(Resource.Drawable.iconCall, 0, 0, 0);
tablayout.GetTabAt(0).SetCustomView(iconlayout1);

کدهای مطلب جاری برای دریافت: Navigation-TabPage-samples.zip
مطالب
شروع کار با Apache Cordova در ویژوال استودیو #4
در قسمت قبل یک مثال ساده را کار کردیم. در این قسمت با jQuery Mobile آشنا شده و در پروژه‌ی خود استفاده خواهیم کرد.

توضیح تکمیلی در مورد ساختار فایل‌های پروژه
همان طور که در قسمتها قبل گفته شد، تگ اسکریپت زیر 
<script src="cordova.js"></script>
از استاندارد‌های Cordova است؛ وجود خارجی ندارد و بخشی از فرآیند ساخت برنامه است.
اگر توجه کنید فایلی با نام platformOverrides.js در فولدر scripts موجود در ریشه، خالی است اما در فولدر merges موجود در ریشه‌ی پروژه مربوط به هر پلتفرم و همنام آن پلتفرم قرار دارد. برای مثال برای android، یک چنین دایرکتوری merges/android/scripts وجود دارد که درون آن فایلی به‌نام  platformOverrides.js دیده می‌شود و اگر دقت کنید، همنام فایل موجود در فولدر scripts موجود در ریشه پروژه است که درون خود فایلی بنام  android2.3-jscompat.js را فراخوانی می‌کند. (برای کمک به سازگاری کتابخانه‌های ثالث)
در زمان build ، تمام فایل‌های موجود در "merges/"platformname ، در فولدر‌های هم نامی در شاخه‌ی ریشه‌ی پروژه کپی شده و جایگزین فایل‌های قبلی خواهند شد.

 مثال برای اندروید 
در زمان ساخت (build) فایل scripts/platformOverrides.js با فایل merges/windows/scripts/platformoverrides.js جایگزین خواهد شد. این امکان برای فلدر‌های css, images و بقیه‌ی آنها نیز امکان پذیر است.
توجه داشته باشید این ادغام در سطح فایل‌ها و نه در سطح محتوای فایل‌ها انجام می‌شود.

نکته 
برای محتوای موجود در فولدر res، قضیه فرق می‌کند. زیرا محتوای این resource‌ها برای اپلیکیشن پکیچ ضروریست؛ پیش از آن که کد‌های ما درون WebView یا host رندر شوند. باید توجه کرد که این فولدر به جهت اینکه منابع اصلی را (با توجه به پلتفرم باید از فایل‌های مشخص آن برای تشخیص ساختار فولدر‌های اپلیکیشن پکیچ استفاده کند) در بر دارد و این منابع باید در زمان ساخت پروژه تشخیص داده شوند.


رویداد‌های بومی
در زیر تعدادی از رخدادهایی که در Cordova گنجانده شده‌اند تا اپلیکیشن ما از رخداد‌های دستگاه با خبر شوند، نشان داده شده است. برای تست آنها به راحتی بعد از اجرای برنامه توسط شبیه ساز Ripple می‌توانید از قسمت Events، رخداد مورد نظر را شبیه سازی کنید:
(function () {
    "use strict";

    document.addEventListener( 'deviceready', onDeviceReady.bind( this ), false );

    function onDeviceReady() {
        // Handle the Cordova pause and resume events
        document.addEventListener( 'pause', onPause.bind( this ), false );
        document.addEventListener('resume', onResume.bind(this), false);
        document.addEventListener('menubutton', onMenuButton.bind(this), false);
        document.addEventListener('backbutton', onBackButton.bind(this), false);
        //document.addEventListener('searchbutton', onResume.bind(this), false);
        //document.addEventListener('endcallbutton', onResume.bind(this), false);
        //document.addEventListener('offline', onResume.bind(this), false);
        //document.addEventListener('online', onResume.bind(this), false);
        //document.addEventListener('startcallbutton', onResume.bind(this), false);
        //document.addEventListener('volumedownbutton', onResume.bind(this), false);
        //document.addEventListener('volumeupbutton', onResume.bind(this), false);
        
        // TODO: Cordova has been loaded. Perform any initialization that requires Cordova here.
    };

    function onPause() {
        // TODO: This application has been suspended. Save application state here.
        alert("paused");
    };

    function onResume() {
        alert("resume");
    };
    function onMenuButton() {
        alert("menu");
    };

    function onBackButton() {
        alert("back button");
    };
   

} )();

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



jQuery Mobile
جی کوئری موبایل، یک فریمورک (UI Framework) جدید با قابلیت استفاده‌ی آسان برای ساخت اپلیکیشن‌های چند سکویی موبایل است. با استفاده از این فریمورک شما قادر خواهید بود اپلیکیشن‌های موبایل بهینه شده برای اجرا بر روی تمام تلفن‌ها، دسکتاپ و تبلت‌ها را بسازید. علاوه بر این، جی کوئری موبایل می‌تواند یک فریمورک ایده آل برای توسعه دهند گان و طراحان وب که قصد ساخت اپلیکیشن‌های غنی وب برای موبایل را دارند، باشد.

 Supported Devices

  Phones/Tablets 
  Android 1.6+ 
  BlackBerry 5+ 
  iOS 3+ 
  Windows Phone 7 
  WebOS 1.4+ 
  Symbian (Nokia S60) 
  Firefox Mobile Opera Mobile 11+ 
  Opera Mini 5+ 
  Desktop browsers 
  Chrome 11+ 
  Firefox 3.6+ 
  Internet Explorer 7+ 
  Safari   


برای نصب jQuery Mobile کافی است دستورات  زیر را در package manager console ویژوال استودیو استفاده کنید:

PM>install-package jquery

PM>install-package jquery.mobile.rtl

بعد از دانلود فایل‌های مورد نظر خود، فولدری بنام jquery.mobile.rtl در ریشه پروژه ایجاد خواهد شد. به ترتیب فایل های rtl.jquery.mobile-1.4.0.css و rtl.jquery.mobile-1.4.0.js موجود در زیر شاخه‌های فلدر مذکور را به head و آخر body فایل index.html اضافه کنید.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>CordovaApp01</title>

    <!-- CordovaApp01 references -->
    <link href="css/index.css" rel="stylesheet" />
    <link href="jquery.mobile.rtl/css/themes/default/rtl.jquery.mobile-1.4.0.css" rel="stylesheet" />
</head>
<body>
    <div data-role="page" id="page1">
        <div data-role="header">
            <h1>اولین برنامه</h1>
        </div>
        <div data-role="content">
            <p>سلام من محتوای اولین برنامه هستم</p>
        </div>
        <div data-role="footer">
            <h1>من فوتر هستم</h1>
        </div>
    </div>
<!-- Cordova reference, this is added to your app when it's built. -->
<script src="scripts/jquery-2.1.3.min.js"></script>
    <script src="cordova.js"></script>
    <script src="scripts/platformOverrides.js"></script>
    <script src="scripts/index.js"></script>

    <script src="jquery.mobile.rtl/js/rtl.jquery.mobile-1.4.0.js"></script>
</body>
</html>
در تکه کد بالا ما یکی از ویجت‌های jQuery Mobile را استفاده کردیم و با استفاده از ویژگی data-role که برای div اصلی با page مقدار دهی شده است، یک  کانتینر (page container) برای ویجت page جی کوئری موبایل تعریف شده‌است.

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

در مقاله‌ی بعد به استفاده از plugin‌ها خواهیم پرداخت.

ادامه دارد...

نظرات مطالب
NuGet 2.0 منتشر شد
اگر موقعی که نسخه قدیمی Nuget نصب باشه اقدام به نصب ASP.NET MVC 4 RC کنید با شکست مواجه خواهید شد هیچ راهی هم جز حذف کردن Nuget ندارید.

بسته MVC خودش حاوی Nuget هست  ونمیدونم چرا نمیتونه آپدیتش کنه!
مطالب
ASP.NET MVC #5

بررسی نحوه انتقال اطلاعات از یک کنترلر به View‌های مرتبط با آن

در ASP.NET Web forms در فایل code behind یک فرم مثلا می‌توان نوشت Label1.Text و سپس مقداری را به آن انتساب داد. اما اینجا به چه ترتیبی می‌توان شبیه به این نوع عملیات را انجام داد؟ با توجه به اینکه در کنترلر‌ها هیچ نوع ارجاع مستقیمی به اشیاء رابط کاربری وجود ندارد و این دو از هم مجزا شده‌اند.
در پاسخ به این سؤال، همان مثال ساده قسمت قبل را ادامه می‌دهیم. یک پروژه جدید خالی ایجاد شده است به همراه HomeController ایی که به آن اضافه کرده‌ایم. همچنین مطابق روشی که ذکر شد، View ایی به نام Index را نیز به آن اضافه کرده‌ایم. سپس برای ارسال اطلاعات از یک کنترلر به View از یکی از روش‌های زیر می‌توان استفاده کرد:

الف) استفاده از اشیاء پویا

ViewBag یک شیء dynamic است که در دات نت 4 امکان تعریف آن میسر شده است. به این معنا که هر نوع خاصیت دلخواهی را می‌توان به این شیء انتساب داد و سپس این اطلاعات در View نیز قابل دسترسی و استخراج خواهد بود. مثلا اگر در اینجا به شیء ViewBag، خاصیت دلخواه Country را اضافه کنیم و سپس مقداری را نیز به آن انتساب دهیم:

using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";
return View();
}
}
}

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

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

در این مثال، @ در View engine جاری که Razor نام دارد، به این معنا می‌باشد که این مقداری است که می‌خواهم دریافت کنی (ViewBag.Country) و سپس آن‌را در حین پردازش صفحه نمایش دهی.


ب) انتقال اطلاعات یک شیء کامل و غیر پویا به View

هر پروژه جدید MVC به همراه پوشه‌ای به نام Models است که در آن می‌توان تعاریف اشیاء تجاری برنامه را قرار داد. در پروژه جاری، یک کلاس ساده را به نام Employee به این پوشه اضافه می‌کنیم:

namespace MvcApplication1.Models
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}

اکنون برای نمونه یک وهله از این شیء را در متد Index ایجاد کرده و سپس به view متناظر با آن ارسال می‌کنیم (در قسمت return View کد زیر مشخص است). بدیهی است این وهله سازی در عمل می‌تواند از طریق دسترسی به یک بانک اطلاعاتی یا یک وب سرویس و غیره باشد.

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Country = "Iran";

var employee = new Employee
{
Email = "name@site.com",
FirstName = "Vahid",
LastName = "N."
};

return View(employee);
}
}
}

امضاهای متفاوت (overloads) متد کمکی View هم به شرح زیر هستند:

ViewResult View(Object model)
ViewResult View(string viewName, Object model)
ViewResult View(string viewName, string masterName, Object model)


اکنون برای دسترسی به اطلاعات این شیء employee در View متناظر با این متد، چندین روش وجود دارد:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country <‪br />
FirstName: @Model.FirstName
</div>

می‌توان از طریق شیء استاندارد دیگری به نام Model (که این هم یک شیء dynamic است مانند ViewBag قسمت قبل)، به خواص شیء یا مدل ارسالی به View جاری دسترسی پیدا کرد که یک نمونه از آن‌را در اینجا ملاحظه می‌کنید.
روش دوم، بر اساس تعریف صریح نوع مدل است به نحو زیر:

@model MvcApplication1.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
Country: @ViewBag.Country
<‪br />
FirstName: @Model.FirstName
</div>

در اینجا در مقایسه با قبل، تنها یک سطر به اول فایل View اضافه شده است که در آن نوع شیء Model تعیین می‌گردد (کلمه model هم در اینجا با حروف کوچک شروع شده است). به این ترتیب اینبار اگر سعی کنیم به خواص این شیء دسترسی پیدا کنیم، Intellisense ویژوال استودیو ظاهر می‌شود. به این معنا که شیء Model بکارگرفته شده اینبار دیگر dynamic نیست و دقیقا می‌داند که چه خواصی را باید پیش از اجرای برنامه در اختیار استفاده کننده قرار دهد.
به این روش، روش Strongly typed view هم گفته می‌شود؛ چون View دقیقا می‌داند که چون نوعی را باید انتظار داشته باشد؛ تحت نظر کامپایلر قرار گرفته و همچنین Intellisense نیز برای آن مهیا خواهد بود.
به همین جهت این روش Strongly typed view، در بین تمام روش‌های مهیا، به عنوان روش توصیه شده و مرجح مطرح است.
به علاوه استفاده از Strongly typed views یک مزیت دیگر را هم به همراه دارد: فعال شدن یک code generator توکار در VS.NET به نام scaffolding. یک مثال ساده:
تا اینجا ما اطلاعات یک کارمند را نمایش دادیم. اگر بخواهیم یک لیست از کارمندها را نمایش دهیم چه باید کرد؟
روش کار با قبل تفاوتی نمی‌کند. اینبار در return View ما، یک شیء لیستی ارائه خواهد شد. در سمت View هم با یک حلقه foreach کار نمایش این اطلاعات صورت خواهد گرفت. راه ساده‌تری هم هست. اجازه دهیم تا خود VS.NET، کدهای مرتبط را برای ما تولید کند.
یک کلاس دیگر به پوشه مدل‌های برنامه اضافه کنید به نام Employees با محتوای زیر:

using System.Collections.Generic;

namespace MvcApplication1.Models
{
public class Employees
{
public IList<Employee> CreateEmployees()
{
return new[]
{
new Employee { Email = "name1@site.com", FirstName = "name1", LastName = "LastName1" },
new Employee { Email = "name2@site.com", FirstName = "name2", LastName = "LastName2" },
new Employee { Email = "name3@site.com", FirstName = "name3", LastName = "LastName3" }
};
}
}
}

سپس متد جدید زیر را به کنترلر Home اضافه کنید.

public ActionResult List()
{
var employeesList = new Employees().CreateEmployees();
return View(employeesList);
}

برای اضافه کردن View متناظر با آن، روی نام متد کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه ظاهر شده:


تیک مربوط به Create a strongly typed view را قرار دهید. سپس در قسمت Model class، کلاس Employee را انتخاب کنید (نه Employees جدید را، چون از آن می‌خواهیم به عنوان منبع داده لیست تولیدی استفاده کنیم). اگر این کلاس را مشاهده نمی‌کنید، به این معنا است که هنوز برنامه را یکبار کامپایل نکرده‌اید تا VS.NET بتواند با اعمال Reflection بر روی اسمبلی برنامه آن‌را پیدا کند. سپس در قسمت Scaffold template گزینه List را انتخاب کنید تا Code generator توکار VS.NET فعال شود. اکنون بر روی دکمه Add کلیک نمائید تا View نهایی تولید شود. برای مشاهده نتیجه نهایی مسیر http://localhost/Home/List باید بررسی گردد.


ج) استفاده از ViewDataDictionary

ViewDataDictionary از نوع IDictionary با کلیدی رشته‌ای و مقداری از نوع object است. توسط آن شیء‌ایی به نام ViewData در ASP.NET MVC به نحو زیر تعریف شده است:

public ViewDataDictionary ViewData { get; set; }

این روش در نگارش‌های اولیه ASP.NET MVC بیشتر مرسوم بود. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["DateTime"] = "<‪br/>" + DateTime.Now;
return View();
}
}
}

و سپس جهت استفاده از این ViewData تعریف شده با کلید دلخواهی به نام DateTime در View متناظر با اکشن Index خواهیم داشت:

@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<div>
DateTime: @ViewData["DateTime"]
</div>

یک نکته امنیتی:
اگر به مقدار انتساب داده شده به شیء ViewDataDictionary دقت کنید، یک تگ br هم به آن اضافه شده است. برنامه را یکبار اجرا کنید. مشاهده خواهید کرد که این تگ به همین نحو نمایش داده می‌شود و نه به صورت یک سطر جدید HTML . چرا؟ چون Razor به صورت پیش فرض اطلاعات را encode شده (فراخوانی متد Html.Encode در پشت صحنه به صورت خودکار) در صفحه نمایش می‌دهد و این مساله از لحاظ امنیتی بسیار عالی است؛ زیرا جلوی بسیاری از حملات cross site scripting یا XSS را خواهد گرفت.
احتمالا الان این سؤال پیش خواهد آمد که اگر «عالمانه» بخواهیم این رفتار نیکوی پیش فرض را غیرفعال کنیم چه باید کرد؟
برای این منظور می‌توان نوشت:
@Html.Raw(myString)

و یا:
<div>@MvcHtmlString.Create("<h1>HTML</h1>")</div>

به این ترتیب خروجی Razor دیگر encode شده نخواهد بود.


د) استفاده از TempData

TempData نیز یک dictionary دیگر برای ذخیره سازی اطلاعات است و به نحو زیر در فریم ورک تعریف شده است:

public TempDataDictionary TempData { get; set; }

TempData در پشت صحنه از سشن‌های ASP.NET جهت ذخیره سازی اطلاعات استفاده می‌کند. بنابراین اطلاعات آن در سایر کنترلرها و View ها نیز در دسترس خواهد بود. البته TempData یک سری تفاوت هم با سشن معمولی ASP.NET دارد:
- بلافاصله پس از خوانده شدن، حذف خواهد شد.
- پس از پایان درخواست از بین خواهد رفت.
هر دو مورد هم به جهت بالابردن کارآیی برنامه‌های ASP.NET MVC و مصرف کمتر حافظه سرور درنظر گرفته‌ شده‌اند.
البته کسانی که برای بار اول هست با ASP.NET مواجه می‌شوند، شاید سؤال بپرسند این مسایل چه اهمیتی دارد؟ پروتکل HTTP، ذاتا یک پروتکل «بدون حالت» است یا Stateless هم به آن گفته می‌شود. به این معنا که پس از ارائه یک صفحه وب توسط سرور، تمام اشیاء مرتبط با آن در سمت سرور تخریب خواهند شد. این مورد متفاوت‌ است با برنامه‌های معمولی دسکتاپ که طول عمر یک شیء معمولی تعریف شده در سطح فرم به صورت یک فیلد، تا زمان باز بودن آن فرم، تعیین می‌گردد و به صورت خودکار از حافظه حذف نمی‌شود. این مساله دقیقا مشکل تمام تازه واردها به دنیای وب است که چرا اشیاء ما نیست و نابود شدند. در اینجا وب سرور قرار است به هزاران درخواست رسیده پاسخ دهد. اگر قرار باشد تمام این اشیاء را در سمت سرور نگهداری کند، خیلی زود با اتمام منابع مواجه می‌گردد. اما واقعیت این است که نیاز است یک سری از اطلاعات را در حافظه نگه داشت. به همین منظور یکی از چندین روش مدیریت حالت در ASP.NET استفاده از سشن‌ها است که در اینجا به نحو بسیار مطلوبی، با سربار حداقل توسط TempData مدیریت شده است.
یک مثال کاربردی در این زمینه:
فرض کنید در متد جاری کنترلر، ابتدا بررسی می‌کنیم که آیا ورودی دریافتی معتبر است یا خیر. در غیراینصورت، کاربر را به یک View دیگر از طریق کنترلری دیگر جهت نمایش خطاها هدایت خواهیم کرد.
همین «هدایت مرورگر به یک View دیگر» یعنی پاک شدن و تخریب اطلاعات کنترلر قبلی به صورت خودکار. بنابراین نیاز است این اطلاعات را در TempData قرار دهیم تا در کنترلری دیگر قابل استفاده باشد:

using System;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
public ActionResult InsertData(string name)
{
// Check for input errors.
if (string.IsNullOrWhiteSpace(name))
{
TempData["error"] = "name is required.";
return RedirectToAction("ShowError");
}
// No errors
// ...
return View();
}

public ActionResult ShowError()
{
var error = TempData["error"] as string;
if (!string.IsNullOrWhiteSpace(error))
{
ViewBag.Error = error;
}
return View();
}
}
}

در همان HomeController دو متد جدید به نام‌های InsertData و ShowError اضافه شده‌اند. در متد InsertData ابتدا بررسی می‌شود که آیا نامی وارد شده است یا خیر. اگر خیر توسط متد RedirectToAction، کاربر به اکشن یا متد ShowError هدایت خواهد شد.
برای انتقال اطلاعات خطایی که می‌خواهیم در حین این Redirect نمایش دهیم نیز از TempData استفاده شده است.
بدیهی است برای اجرا این مثال نیاز است دو View جدید برای متدهای InsertData و ShowError ایجاد شوند (کلیک راست روی نام متد و انتخاب گزینه Add view برای اضافه کردن View مرتبط با آن اکشن).
محتوای View مرتبط با متد افزودن اطلاعات فعلا مهم نیست، ولی View نمایش خطاها در ساده‌ترین حالت مثلا می‌تواند به صورت زیر باشد:

@{
ViewBag.Title = "ShowError";
}

<h2>Error</h2>

@ViewBag.Error

برای آزمایش برنامه هم مطابق مسیریابی پیش فرض و با توجه به قرار داشتن در کنترلری به نام Home، مسیر http://localhost/Home/InsertData ابتدا باید بررسی شود. چون آرگومانی وارد نشده، بلافاصله صفحه به آدرس http://localhost/Home/ShowError به صورت خودکار هدایت خواهد شد.


نکته‌ای تکمیلی در مورد Strongly typed viewها:
عنوان شد که Strongly typed view روش مرجح بوده و بهتر است از آن استفاده شود، زیرا اطلاعات اشیاء و خواص تعریف شده در یک View تحت نظر کامپایلر قرار می‌گیرند که بسیار عالی است. یعنی اگر در View بنویسم FirstName: @Model.FirstName1 چون FirstName1 وجود خارجی ندارد، برنامه نباید کامپایل شود. یکبار این را بررسی کنید. برنامه بدون مشکل کامپایل می‌شود! اما تنها در زمان اجرا است که صفحه زرد رنگ معروف خطاهای ASP.NET ظاهر می‌شود که چنین خاصیتی وجود ندارد (این حالت پیش فرض است؛ یعنی کامپایل یک View‌ در زمان اجرا). البته این باز هم خیلی بهتر است از ViewBag، چون اگر مثلا ViewBag.Country1 را وارد کنیم، در زمان اجرا تنها چیزی نمایش داده نخواهد شد؛‌ اما با روش Strongly typed view، حتما خطای Compilation Error به همراه نمایش محل مشکل نهایی، در صفحه ظاهر خواهد شد.
سؤال: آیا می‌شود پیش از اجرای برنامه هم این بررسی را انجام داد؟
پاسخ: بله. باید فایل پروژه را اندکی ویرایش کرده و مقدار MvcBuildViews را که به صورت پیش فرض false هست، true نمود. یا خارج از ویژوال استودیو با یک ادیتور متنی ساده مثلا فایل csproj را گشوده و این تغییر را انجام دهید. یا داخل ویژوال استودیو، بر روی نام پروژه کلیک راست کرده و سپس گزینه Unload Project را انتخاب کنید. مجددا بر روی این پروژه Unload شده کلیک راست نموده و گزینه edit را انتخاب نمائید. در صفحه باز شده، MvcBuildViews را یافته و آن‌را true کنید. سپس پروژه را Reload کنید.
اکنون اگر پروژه را کامپایل کنید، پیغام خطای زیر پیش از اجرای برنامه قابل مشاهده خواهد بود:

'MvcApplication1.Models.Employee' does not contain a definition for 'FirstName1' 
and no extension method 'FirstName1' accepting a first argument of type 'MvcApplication1.Models.Employee'
could be found (are you missing a using directive or an assembly reference?)
d:\Prog\MvcApplication1\MvcApplication1\Views\Home\Index.cshtml 10 MvcApplication1

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

و یک خبر خوب!
مجوز سورس کد ASP.NET MVC از MS-PL به Apache تغییر کرده و همچنین Razor و یک سری موارد دیگر هم سورس باز شده‌اند. این تغییرات به این معنا خواهند بود که پروژه از حالت فقط خواندنی MS-PL به حالت متداول یک پروژه سورس باز که شامل دریافت تغییرات و وصله‌ها از جامعه برنامه نویس‌ها است، تغییر کرده است (^ و ^).

مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 19 - بومی سازی
هدف از زیر ساخت بومی سازی در ASP.NET Core، حذف عبارات و رشته‌های درج شده‌ی در کلاس‌ها و ویووهای مختلف برنامه و انتقال آن‌ها به فایل‌های منبع resx است و سپس استفاده‌ی از آن‌ها توسط تزریق وابستگی‌ها. به این ترتیب می‌توان بر اساس نوع فرهنگ درخواستی کاربر جاری، رشته‌های درج شده را به صورت پویا، در زمان اجرای برنامه، بر اساس ترجمه‌های آن‌ها به کاربر نمایش داد.


نحوه‌ی تعیین فرهنگ ترد جاری در ASP.NET Core

در نگارش‌های پیشین ASP.NET، برای تعیین فرهنگ ترد جاری، از یکی از دو روش ذیل استفاده می‌شود:
الف) افزودن مدخل بومی سازی به فایل web.config
<system.web>
    <globalization uiCulture="fa-IR" culture="fa-IR" />
</system.web>
ب) و یا تعیین فرهنگ ترد با کدنویسی مستقیم در فایل global.asax
protected void Application_BeginRequest()
{
   Thread.CurrentThread.CurrentCulture = new CultureInfo("fa-IR");
   Thread.CurrentThread.CurrentUICulture = new CultureInfo("fa-IR");
}
در ASP.NET Core با حذف شدن System.Web و همچنین فایل global.asax، برای تعیین فرهنگ پیش فرض ترد جاری، به همراه فرهنگ‌هایی که برنامه از آن‌ها پشتیبانی می‌کند، به صورت ذیل عمل می‌شود:
public void Configure(IApplicationBuilder app)
{
    app.UseRequestLocalization(new RequestLocalizationOptions
    {
        DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
        SupportedCultures = new[]
        {
            new CultureInfo("en-US"),
            new CultureInfo("fa-IR")
        },
        SupportedUICultures = new[]
        {
            new CultureInfo("en-US"),
            new CultureInfo("fa-IR")
        }
    });
در اینجا با مراجعه به کلاس آغازین برنامه و افزودن تنظیمات میان افزار RequestLocalization، می‌توان فرهنگ پیش فرض درخواست جاری و یا فرهنگ‌های پشتیبانی شده را مشخص کرد.
- تنظیمات SupportedCultures بر روی نمایش تاریخ، ساعت و واحد پولی تاثیر دارند. همچنین می‌توانند بر روی نحوه‌ی مقایسه‌ی حروف و مرتب سازی آن‌ها تاثیر داشته باشند.
- تنظیمات SupportedUICultures مشخص می‌کنند که کدامیک از فایل‌های resx برنامه که مداخل ترجمه‌های آن‌را به زبان‌های مختلف مشخص می‌کنند، باید بارگذاری شوند.
- تنظیم DefaultRequestCulture در صورت مشخص نشدن فرهنگ ترد جاری مورد استفاده قرار می‌گیرد.

یک مثال: هر ترد در دات نت دارای اشیاء CurrentCulture و CurrentUICulture است. اگر فرهنگ ترد جاری به en-US تنظیم شده باشد، متد DateTime.Now.ToLongDateString، خروجی نمونه Thursday, February 18, 2016 را نمایش می‌دهد.


زمانیکه میان افزار RequestLocalization فعال می‌شود، سه تامین کننده‌ی پیش فرض (مقدار‌های پیش فرض خاصیت RequestCultureProviders شیء RequestLocalizationOptions فوق)، جهت مشخص ساختن فرهنگ ترد جاری بکار گرفته خواهند شد:
الف) از طریق کوئری استرینگ با فعال سازی QueryStringRequestCultureProvider
http://localhost:5000/?culture=es-MX&ui-culture=es-MX
http://localhost:5000/?culture=es-MX
برای مثال در اینجا QueryStringRequestCultureProvider به دنبال کوئری استرینگ‌های culture و یا ui-culture گشته و با رسیدن به es-MX، فرهنگ جاری را به اسپانیایی مکزیکی تنظیم می‌کند. در این حالت اگر فقط culture ذکر شود، ui-culture نیز به همان مقدار تنظیم خواهد شد.
ب) از طریق نام کوکی با فعال سازی CookieRequestCultureProvider
CookieRequestCultureProvider کوکی ویژه‌ای را با نام پیش فرض AspNetCore.Culture. ایجاد می‌کند. این کوکی برای ردیابی اطلاعات بومی سازی انتخابی کاربر بکار می‌رود. برای مثال اگر به مقدار ذیل تنظیم شود:
 c='en-UK'|uic='en-US'
c آن به معنای culture و uic آن به معنای ui-culture خواهد بود.
ج) از طریق هدر مخصوص Accept-Language با فعال سازی AcceptLanguageHeaderRequestCultureProvider که می‌تواند به همراه درخواست HTTP ارسال شود.

اگر تمام این حالت‌ها تنظیم نشده بودند، آنگاه از مقدارDefaultRequestCulture  استفاده می‌شود. برای مثال اگر مرورگر به صورت پیش فرض هدر Accept-Language را en-US ارسال می‌کند :


دیگر کار به پردازش مقدارDefaultRequestCulture  نخواهد رسید.

اکنون اگر علاقمند بودید تا به کاربر امکان انتخاب زبانی را بدهید، یک چنین اکشن متدی را طراحی کنید:
public IActionResult SetFaLanguage()
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );
 
    return RedirectToAction("GetTitle");
}
این اکشن متد بر اساس تامین کننده‌ی کوکی ردیابی زبان انتخاب شده‌ی توسط کاربر و یا CookieRequestCultureProvider کار می‌کند و توسط آن، فرهنگ جاری برنامه به زبان فارسی تنظیم می‌شود. هرگاه که این اکشن متد فراخوانی شود، کوکی AspNetCore.Culture. به مقدار c=fa-IR|uic=fa-IR تنظیم می‌شود:


از اینجا به بعد است که اگر نام کنترلر شما TestLocalController باشد، فایل منبع متناظر با آن یعنی Controllers.TestLocalController.fa.resx، به صورت خودکار بارگذاری و پردازش خواهد شد. در غیر اینصورت فایل نمونه‌ی ختم شده‌ی به en.resx پردازش می‌شود؛ چون این زبان به صورت پیش فرض در هدر Accept-Language قید شده‌است.


آماده سازی برنامه برای کار با فایل‌های منبع زبان‌های مختلف

ابتدا پوشه‌ی جدیدی را به نام Resources به ریشه‌ی پروژه اضافه کنید. سپس به کلاس آغازین برنامه مراجعه کرده و محل یافت شدن این پوشه را معرفی کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddLocalization(options => options.ResourcesPath = "Resources");
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();
در اینجا سرویس جدید Localization، به لیست سرویس‌های ثبت شده‌ی در IoC Container اضافه می‌شود. همچنین توسط خاصیت  ResourcesPath  آن مشخص شده‌است که فایل‌های resx را باید از کجا دریافت کند.
به علاوه به سرویس ASP.NET MVC، تنظیمات بومی سازی Viewها و DataAnnotations نیز اضافه شده‌اند. تنظیم suffix به معنای  view file suffix و یا مثلا fr در نام فایل Index.fr.cshtml است.


نحوه‌ی تعریف و پوشه بندی فایل‌های منبع زبان‌های مختلف

تا اینجا پوشه‌ی جدید Resources را به پروژه اضافه، معرفی و سرویس‌های مرتبط را نیز فعال کردیم. پس از آن نوبت به افزودن فایل‌های resx است. برای این منظور بر روی پوشه‌ی منابع کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید.


در اینجا با جستجوی resource، می‌توان فایل resx جدیدی را به پروژه اضافه کرد؛ اما ... انتخاب نام آن باید بر اساس نکات ذیل باشد:
الف) برای کنترلرها یکی از دو مسیر / دار و یا نقطه دار جستجو می‌شوند:
Resources/Controllers.HomeController.fr.resx
Resources/Controllers/HomeController.fr.resx

در اینجا fr ذکر شده، همان LanguageViewLocationExpanderFormat.Suffix است که پیشتر بحث شد. قسمت ابتدایی Controllers همیشه ثابت است (یا به صورت نام یک پوشه و یا به عنوان قسمت اول نام فایل). سپس نام کلاس کنترلر به همراه نام فرهنگ مدنظر باید ذکر شوند. قسمت نام پوشه‌ی Resources را نیز به services.AddLocalization معرفی کرده‌ایم.

ب) برای Viewها نیز همان حالت‌های / دار و یا نقطه دار بررسی می‌شوند:
Resources/Views.Home.About.fr.resx
Resources/Views/Home/About.fr.resx


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

در این نگارش از ASP.NET، در حالت کلی، نام یک فایل منبع، همان نام کامل کلاس آن است؛ منهای فضای نام آن (اگر این فایل منبع در همان اسمبلی قرار گیرد). برای مثال اگر می‌خواهید برای کلاس Startup برنامه، فایل منبعی را درست کنید و نام کامل آن با درنظر گرفتن فضای نام، معادل LocalizationWebsite.Web.Startup است، ابتدای فضای نام آن‌را حذف کنید و سپس آن‌را ختم به fa.resx کنید؛ مثلا Startup.fa.resx
اگر محل واقع شدن فایل‌های resx در همان اسمبلی اصلی پروژه باشند، نیازی به ذکر فضای نام پیش فرض پروژه نیست. برای مثال اگر فضای نام پیش فرض پروژه‌ی وب جاری MyLocalizationWebsite.Web است، بجای نام فایل MyLocalizationWebsite.Web.Controllers.HomeController.fr.resx می‌توانید به صورت خلاصه بنویسید Controllers.HomeController.fr.resx. در غیراینصورت (استفاده از اسمبلی‌های دیگر)، ذکر کامل فضای نام مرتبط هم الزامی است.


چند نکته:
- اگر ResourcesPath را در services.AddLocalization معرفی نکنید، مسیر پیش فرض یافتن فایل‌های resx مربوط به کنترلرها، پوشه‌ی ریشه‌ی پروژه است و برای Viewها، همان پوشه‌ی محل واقع شدن View متناظر خواهد بود.
- اینکه کدام فایل منبع در برنامه بارگذاری می‌شود، دقیقا مرتبط است با فرهنگ ترد جاری و این فرهنگ به صورت پیش فرض en-US است (چون همواره در هدر Accept-Language ارسالی توسط مرورگر وجود دارد). برای تغییر آن، از نکته‌ی اکشن متد public IActionResult SetFaLanguage ابتدای بحث استفاده کنید (در غیراینصورت در آزمایشات خود شاهد بارگذاری فایل‌های منبع دیگری بجز en.resx‌ها نخواهید بود).
- فایل‌های منبع را به صورت کامپایل شده در پوشه‌ی bin برنامه خواهید یافت:



خواندن اطلاعات منابع در کنترلرهای برنامه

فرض کنید کنترلری را به نام TestLocalController ایجاد کرده‌ایم. بنابراین فایل منبع فارسی متناظر با آن Controllers.TestLocalController.fa.resx خواهد بود؛ با این محتوای نمونه:


محتوای این کنترلر نیز به صورت ذیل است:
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class TestLocalController : Controller
    {
        private readonly IStringLocalizer<TestLocalController> _stringLocalizer;
        private readonly IHtmlLocalizer<TestLocalController> _htmlLocalizer;
 
        public TestLocalController(
            IStringLocalizer<TestLocalController> stringLocalizer,
            IHtmlLocalizer<TestLocalController> htmlLocalizer)
        {
            _stringLocalizer = stringLocalizer;
            _htmlLocalizer = htmlLocalizer;
        }
 
        public IActionResult Index()
        {
            var name = "DNT";
            var message = _htmlLocalizer["<b>Hello</b><i> {0}</i>", name];
            ViewData["Message"] = message;
            return View();
        }
 
        [HttpGet]
        public string GetTitle()
        {
            var about = _stringLocalizer["About Title"];
            return about;
        }
 
        public IActionResult SetFaLanguage()
        {
            Response.Cookies.Append(
                CookieRequestCultureProvider.DefaultCookieName,
                CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(new CultureInfo("fa-IR"))),
                new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
            );
 
            return RedirectToAction("GetTitle");
        }
    }
}
در اینجا نحوه‌ی دسترسی به فایل‌های منبع را در کنترلرها مشاهده می‌کنید. سرویس IStringLocalizer برای خواندن key/valueهای معمولی طراحی شده‌است و سرویس IHtmlLocalizer برای خواندن key/valueهای تگ دار، بکار می‌رود. علت تنظیم شدن پارامتر جنریک آن‌ها به نام کنترلر جاری این است که این سرویس‌ها بدانند دقیقا چه نوعی را قرار است بارگذاری کنند و دقیقا باید به دنبال کدام فایل بگردند. این سرویس‌ها یک کلید را می‌گیرند و یک خروجی و مقدار را باز می‌گردانند.
اگر برنامه را در حالت معمولی اجرا کنید و سپس آدرس http://localhost:7742/testlocal/gettitle را درخواست کنید، عبارت About Title را مشاهده می‌کنید؛ به دو علت:
الف) هنوز فرهنگ پیش فرض ترد جاری همان en-US است که توسط مرورگر ارسال شده‌است.
ب) چون فایل resx متناظر با فرهنگ پیش فرض ترد جاری یافت نشده‌است، مقدار همان کلید درخواستی بازگشت داده می‌شود؛ یعنی همان About Title.

برای رفع این مشکل آدرس http://localhost:7742/testlocal/SetFaLanguage را درخواست کنید. به این صورت با تنظیم کوکی ردیابی فرهنگ ترد جاری به زبان فارسی، خروجی GetTile این‌بار «درباره» خواهد بود.


خواندن اطلاعات منابع در Viewهای برنامه

فرض کنید فایل Views.TestLocal.Index.fa.resx (فایل منبع کنترلر TestLocal و ویوو Index آن به زبان فارسی) دقیقا همان محتوای فایل Controllers.TestLocalController.fa.resx فوق را دارد (اگر نام پوشه‌ی Views را تغییر داده‌اید، قسمت ابتدایی نام فایل Views را هم باید تغییر دهید). برای دسترسی به اطلاعات آن در یک ویوو، می‌توان از سرویس IViewLocalizer  به نحو ذیل استفاده کرد:
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
 
@{
}
Message @ViewData["Message"]
<br/>
@Localizer["<b>Hello</b><i> {0}</i>", "DNT"]
<br/>
@Localizer["About Title"]
در اینجا ViewData، از همان اطلاعات اکشن متد Index استفاده می‌کند.
Localizer از طریق تزریق سرویس IViewLocalizer  به View برنامه تامین می‌شود. این سرویس در پشت صحنه از همان IHtmlLocalizer استفاده می‌کند و در حین استفاده‌ی از آن، اطلاعات تگ‌ها انکد (encoded) نخواهند شد (به همین جهت برای کار با کلیدها و مقادیر تگ‌دار توصیه می‌شود).


استفاده از اطلاعات منابع در DataAnnotations

قسمت اول فعال سازی بومی سازی DataAnnotations با ذکر AddDataAnnotationsLocalization در متد ConfigureServices، در ابتدای بحث انجام شد و همانطور که پیشتر نیز عنوان گردید، در این نگارش از ASP.NET، برای تمام کلاس‌های برنامه می‌توان فایل منبع ایجاد کرد. برای مثال اگر کلاس RegisterViewModel در فضای نام ViewModels.Account قرار گرفته‌است، نام فایل منبع آن یکی از دو حالت / دار و یا نقطه دار ذیل می‌تواند باشد:
Resources/ViewModels.Account.RegisterViewModel.fr.resx
Resources/ViewModels/Account/RegisterViewModel.fr.resx

محتوای این کلاس را در ذیل مشاهده می‌کنید:
using System.ComponentModel.DataAnnotations;
 
namespace Core1RtmEmptyTest.ViewModels.Account
{
    public class RegisterViewModel
    {
        [Required(ErrorMessage = "EmailReq")]
        [EmailAddress(ErrorMessage = "EmailType")]
        [Display(Name = "Email")]
        public string Email { get; set; }
    }
}
در این حالت مقداری که برای ErrorMessage ذکر می‌شود، کلیدی است که باید در فایل منبع جستجو شود:


یک نکته: هیچ الزامی ندارد که کلیدها را به این شکل وارد کنید. از این جهت که اگر این کلید در فایل منبع یافت نشد و یا فرهنگ ترد جاری با فایل‌های منبع مهیا تطابقی نداشت، عبارتی را که کاربر مشاهده می‌کند، دقیقا معادل «EmailReq» خواهد بود. بنابراین در اینجا می‌توانید کلید را به صورت کامل، مثلا مساوی «The Email field is required» وارد کنید و همین عبارت را به عنوان کلید در فایل منبع ذکر کرده و مقدار آن‌را مساوی ترجمه‌ی آن قرار دهید. این نکته در تمام حالات کار با کنترلرها و ویووها نیز صادق است.


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

اگر می‌خواهید تعدادی از منابع را در همه‌جا در اختیار داشته باشید، روش کار به این صورت است:
الف) یک کلاس خالی را به نام SharedResource دقیقا با فرمت ذیل در پوشه‌ی Resources ایجاد کنید:
// Dummy class to group shared resources
namespace Core1RtmEmptyTest
{
   public class SharedResource
   {
   }
}
ب) اکنون فایل‌های منبع خود را در پوشه‌ی Resources، دقیقا با این نام‌های خاص ایجاد کنید:
SharedResource.fa.resx
SharedResource.en-US.resx
و امثال آن

ج) برای استفاده‌ی از این منبع اشتراکی در کلاس‌های مختلف برنامه تنها کافی است در حین تزریق وابستگی‌ها، نوع آرگومان جنریک IStringLocalizer را به SharedResource تنظیم کنید:
 IStringLocalizer<SharedResource> sharedLocalizer
و یا حتی در ویووهای برنامه نیز می‌توان از آن استفاده کرد:
 @inject IHtmlLocalizer<SharedResource> SharedLocalizer