نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها
یک نکته‌ی تکمیلی: در ASP.NET Core 3.0 فراموش شدن ثبت سرویس‌ها در ابتدای اجرای برنامه گوشزد می‌شود

فرض کنید WeatherForecastService شما به DataService وابستگی دارد:
public class WeatherForecastService
{
    private readonly DataService _dataService;
    public WeatherForecastService(DataService dataService)
    {
        _dataService = dataService;
    }
و اکنون از این سرویس در یک کنترلر استفاده کرده‌اید:
public class WeatherForecastController : ControllerBase
{
    private readonly WeatherForecastService _service;
    public WeatherForecastController(WeatherForecastService service)
    {
        _service = service;
    }
و در این بین، در حین معرفی وابستگی‌ها:
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSingleton<WeatherForecastService>();
ثبت سرویس Data فراموش شده‌است. اکنون اگر برنامه را اجرا کنید، پیش از شروع به کار، این اعتبارسنجی رخ خواهد داد:
Unhandled exception. System.AggregateException: Some services are not able to be constructed
(Error while validating the service descriptor 
    'ServiceType: TestApp.WeatherForecastService Lifetime: Scoped ImplementationType:
     TestApp.WeatherForecastService': Unable to resolve service for type
    'TestApp.DataService' while attempting to activate 'TestApp.WeatherForecastService'.)
به این ترتیب قبل از شروع برنامه، کمبودهای تنظیمات سیستم تزریق وابستگی‌ها گوشزد می‌شود.
پروژه‌ها
بات تلگرامی پیدا کردن افردا در شعاع 10 کیلومتری
با توجه به بات‌های موجود در زمینه چت و  جستجو افراد تصمیم گرفتم پروژه مهندسی نرم افزاری خود را شبیه بات‌های موجود بعلاوه امکانات اضافه‌تر و ایده‌ای جدید بنویسم.
GoogleMapBot تنها یک بات تلگرامی نمیباشد. شما با دانلود سورس کد پروژه میتوانید دستگاه گوشی خودتان را وصل کنید و از همان سرویس استفاده کنید.
همانطور که از نام بات مشخص میباشد Googlemap بخش مهمی در این پروژه ایفا میکند.
کاربران آنلاین بات به محض لاگین شدن در بات، عکس پروفایل و مکان فعلی خود و سایر کاربران را میتوانند به صورت آنی بر روی صفحه html مشاهده نمایند.
سمت سرور پروژه با Web Api  و کلاینت آن با جی‌کوئری ایجکس پیاده سازی شده است. همچین با استفاده ازSignalR کاربران را هنگام ورود و خروج بر روی گوگل مپ به صورت آنی مشخص کرده‌ایم.
از امکانات آن میتوان به موارد زیر اشاره کرد
  • چت روم: هر منطقه دارای یک چت روم میباشد که بدون تداخل با همدیگر میتوانند کار خود را انجام دهند.
  • وب سایت: بات داری وبسایت میباشد که در بات میتوان با کلیک بر دکمه آن وارد وبسایت شد و درون وب میتوان نظرات خود را برای ادمین فرستاد.
  • ذخیره شدن  اطلاعات کاربران: تمام چت‌ها، عکس‌ها و حتی مکان‌های کاربر در بانک ذخیره میشود.
  • قابلیت آپدیت:  هنگام آپدیت برنامه، کاربران کاملا مدیریت شده و تداخلی در برنامه ایجاد نخواهد شد و پیغامی به تمام کاربران فرستاده میشود.
  • زمان بندی کاربران:  امکانی که در هیچ کدام از بات‌های دیگر موجود نیست، مدیریت کاربران آنلاین  میباشد. GooglemapBot با استفاده از  الگوریتمی که برای آن طراحی شده است، میتواند بعد از زمان مشخصی در صورتیکه کاربر فعالیت نداشته باشد، او را از بات خارج کند. 
  • نمایش افراد آنلاین: یکی دیگر از قابلیت‌های بات، امکان نمایش کاربران بر روی نقشه میباشد. هر کاربری بعد از ورود و خروج به صورت آنی با عکس پروفایلش بر روی نقشه نمایش داده میشود.
  •  محاسبه دقیق فاصله: هنگام ورود به بات، فاصله دقیق کاربر تا تهران محاسبه شده و نمایش داده میشود.
  • پنل مدیریت: داخل بات مدیر با وارد کردن پسورد خود شاهد پنل مدیریتی خواهد بود و با خروج پنل بات نمایش داده میشود.
امکانات بالا جزو سرویس‌هایی بودند که در بات‌های مشابهی که مشاهده کردم، وجود نداشتند و از  ذکر امکانات مشابه خوداری کردم. شما میتوانید سورس آن را دانلود کنید و آن را به شیوه دلخواه خود طراحی کنید. 
تصاویر برنامه


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

- غیرفعال کردن سرویس آپدیت ویندوز:

هر دو مورد را دستی هم می‌توانید غیرفعال کنید. در قسمت run ویندوز بنویسید services.msc و سپس enter. سپس دو سرویس فوق را یافته، آن‌ها را ابتدا stop و بعد disable کنید.
مطالب
ارتقاء به HTTP Client در Angular 4.3
عموما در برنامه‌های SPA، اطلاعات از طریق HTTP و از طرف سرور دریافت می‌شوند. از نگارش‌های ابتدایی Angular، اینکار از طریق HTTP Module آن مسیر بود و هست. در Angular 4.3 روش بهبودیافته‌ای نسبت به این روش متداول ارائه شده‌است که در ادامه تعدادی از ویژگی‌های مقدماتی آن‌را بررسی می‌کنیم.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار می‌کنند؛ اما طراحی جدید آن شامل ویژگی‌های توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.


به روز رسانی وابستگی‌های پروژه

پیش از هرکاری نیاز است وابستگی‌های پروژه را به روز رسانی کرد و یکی از روش‌های ساده‌ی یافتن شماره نگارش‌های جدید بسته‌های تعریف شده‌ی در فایل package.json برنامه، استفاده از بسته‌ی npm-check-updates است:
npm install npm-check-updates -g
ncu
دستور اول، این بسته را به صورت عمومی نصب کرده و صدور دستور دوم در ریشه‌ی پروژه، سبب می‌شود تا گزارشی از آخرین به روز رسانی‌ها، نمایش داده شود (بدون هیچگونه تغییری در پروژه):


در اینجا شماره نگارش‌های جدید مشخص شده‌اند و همچنین روش سریع ارتقاء به آن‌ها نیز ذکر شده‌است. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانی‌ها توسط ncu انجام شوند:
ncu -a
npm update
دستور اول صرفا شماره نگارش‌های جدید را در فایل package.json، به صورت خودکار اصلاح می‌کند و دستور دوم سبب دریافت، نصب و اعمال آن‌ها خواهد گردید.


تغییرات مورد نیاز جهت معرفی ماژول HttpClient

این ماژول جدید از طریق اینترفیس HttpClientModule ارائه می‌شود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
 import { HttpModule } from '@angular/http';
به:
 import { HttpClientModule } from '@angular/common/http';

پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه می‌کنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({
    imports: [
        // ...
        HttpClientModule,
        // ...
    ],
declarations: [ ... ],
providers: [ ... ],
exports: [ ... ]
})
export class AppModule { }


تغییرات مورد نیاز در سازنده‌ها و تزریق وابستگی‌ها

پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازنده‌ی کلاسی تزریق شده‌است، یافته و به صورت ذیل تغییر دهیم:
از:
 constructor(private http: Http) { }
به:
import { HttpClient } from '@angular/common/http';
// ...
constructor(private http: HttpClient) { }


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Verbs

یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => {
    return this.http.get(url)
        .map((response: Response) => <MyType>response.json());
}
ابتدا درخواستی ارسال شده و سپس نتیجه‌ی آن به JSON تبدیل گشته و در آخر به نوع بازگشتی متد تبدیل می‌شود.
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیش‌فرض نوع بازگشتی از سرور JSON فرض می‌شود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا می‌توان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد ساده‌ی ذیل تبدیل می‌شود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
}
برای نمونه شبیه به همین نکته برای post نیز صادق است:
post<T>(url: string, body: string): Observable<T> {
   return this.http.post<T>(url, body);
}

نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
 getData() {
  this.http.get(this.url, { responseType: 'text' }).subscribe(res => {
       this.data = res;
  });
}
در این‌حالت خروجی متنی <Observable<string را دریافت می‌کنیم و نیازی به ذکر <get<string نیست.

نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{
  "results": [
    "Item 1",
    "Item 2",
  ]
}
در این حالت برای دسترسی به اطلاعات این فیلد می‌توان از حالت key/value بودن اشیاء جاوا اسکریپتی به شکل زیر برای دسترسی به خاصیت results استفاده کرد:
this.http.get('/api/items').subscribe(data => {
   this.results = data['results'];
});


نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت

این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر می‌شوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت می‌دهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا می‌کنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی می‌شوند.


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Headers

در اینجا برای تعریف headers می‌توان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http";

const headers = new HttpHeaders({ "Content-Type": "application/json" });
و یا به صورت fluent به شکل زیر:
 const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');

سپس آن‌را به عنوان پارامتر سوم، به متدهای http ارسال می‌کنیم. یک مثال:
  updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> {
    const header = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .put<AppProduct>(
        `${this.baseUrl}/UpdateProduct/${id}`,
        JSON.stringify(item),
        { headers: header }
      )
      .map(response => response || {});
  }

تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمده‌است. به همین جهت است که در اینجا یک {} را نیز مشاهده می‌کنید:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>

یک نکته: شیء HttpHeaders به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
headers = headers.set('Accept', 'application/json');
headers تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
 const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
;


امکان تعریف HttpParams

اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن می‌توان جهت تعریف کوئری استرینگ‌ها استفاده کرد. برای مثال درخواست ذیل:
http
  .post('/api/items/add', body, {
      params: new HttpParams().set('id', '3'),
  })
  .subscribe();
سبب تولید یک چنین URL ایی می‌گردد:
  /api/items/add?id=3

یک نکته: شیء HttpParams به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const params = new HttpParams();
params.set('orderBy', '"$key"')
params.set('limitToFirst', "1");
params تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
const params = new HttpParams()
.set('orderBy', '"$key"')
.set('limitToFirst', "1");
به علاوه روش تعریف ذیل نیز برای کار با HttpParams مجاز است:
 const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});


تغییرات مورد نیاز در کدهای سرویس‌ها جهت مدیریت خطاها

در اینجا اینبار خطای بازگشتی، از نوع ویژه‌ی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل می‌باشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http";

postData() { 
  this.http.post(this.url, this.payload).subscribe( 
    res => { 
      console.log(res); 
    }, 
    (err: HttpErrorResponse) => { 
      console.log(err.error); 
      console.log(err.name); 
      console.log(err.message); 
      console.log(err.status); 

        if (err.error instanceof Error) { 
          console.log("Client-side error occured."); 
        } else { 
          console.log("Server-side error occured."); 
        }
    } 
  ); 
}


امکان سعی مجدد در اتصال توسط HTTP Client

ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry می‌توان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import می‌کنیم:
 import 'rxjs/add/operator/retry';
سپس:
http
  .get<ItemsResponse>('/api/items')
  .retry(3)
  .subscribe(...);
این کد در صورت بروز خطایی، این عملیات را سه بار تکرار می‌کند. در انتها اگر بازهم خطایی دریافت شد، این خطا را به برنامه بازگشت می‌دهد.


امکان درخواست کل Response بجای Body

اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شده‌است. به این معنا که تنها body یک response را تبدیل به یک شیء JSON می‌کند:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>
اما گاهی از اوقات نیاز است تا به کل Response دسترسی داشته باشیم. در این حالت باید نوع observe را به response تنظیم کرد:
http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    console.log(resp.headers.get('X-Custom-Header'));
    console.log(resp.body.someField);
  });
به این ترتیب اینبار resp از نوع <HttpResponse<MyJsonData خواهد بود که توسط آن می‌توان به خواص headers و یا body، به صورت جداگانه‌ای دسترسی یافت.


یک نکته‌ی تکمیلی: کدهای سری کار با فرم‌ها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصه‌ی تغییرات آن‌ها به این صورت خواهند بود.
نظرات مطالب
NoSQL ؟

با سلام

من به عنوان کسی که در پروژه‌های خود از انوع ذخیره سازی‌ها بر اساس نیاز استفاده کردم(سرعت! راحتی! پلتفرم ها! و...) هم نظر میدم و هم پاسخ شما دوست عزیز را می‌دم.

قطعا انتخاب اینکه از چه روشی برای ذخیره سازی داده‌ها استفاده شود بسته به تیم پیاده سازکننده پروژه و نیز طراحان و... دارد. من با یک مثال توضیحی را خدمت شما می‌دهم.

در یک پروژه که اخیرا در حال اجرا هست(در دست من و هم تیمی‌های من) این پروژه یک پروژه بزرگ و با دیدها و اهداف وسیعی هست. ما در این برنامه هم از ادرس دهی بر اساس پوشه‌ها و دایرکتوری‌ها داده‌ها را ذخیره کردیم(اطلاعاتی مانند لینک فایل‌ها و یا تصاویر و...) و حتی در بعضی محل‌ها نیاز بود که اطلاعات یک فرد را در یک فایل xml قرار می‌دادیم و بعضی وقتها هم در پایگاه داده و هم فایل xml به این دلیل که در مورد اول تنها برنامه سمت کلاینت نیاز به این اطلاعات داشت و در آنجا پارسر قوی xml وجود داشت اما در مورد دوم ما به یک سری دیتا نیاز داشتیم که هم در سرور به آنها نیاز داریم و هم کلاینت! خب در بحث وب ما به مدیران اگر می‌خواستیم xml ارائه کنیم قطعا راه حل خوبی نبود و از سرعت و کارایی ما کم می‌کرد لذا از پایگاه داده استفاده کردم ولی برای زمانی که کاربر کلاینتی ما نیاز به اطلاعات داشت به این دلیل که بار سرور زیاد نشود از xml‌ها استفاده می‌شد که با یک لینک مستقیم می‌توانست به دست اورد(البته خود لینک همین فایل xml هم ساخته می‌شد! هیچ جا ذخیره نمی‌شد!) 

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

از نویسنده مطلب بابت مطلب خوبشون که کم دیدم در تارنماهای فارسی به اون بپردازن(متاسفانه بسیاری از اساتید دانشگاهی با این مفهوم حتی اشنایی ندارند با اینکه دانستن کلیت ان یک تعریف ساده است!) موفق باشید.

مطالب
آموزش Xamarin Forms - قسمت اول - نصب و راه اندازی اولین پروژه زمارین

در  مقاله قبلی ، درباره دلایل استفاده از Xamarin ، کمی صحبت کردیم. حال وقت آن رسیده که به سراغ نصب و راه اندازی اولین پروژه زمارین برویم.

اگر شما از Visual Studio 2015  به بعد استفاده کنید، موقع نصب قادر خواهید بود که با انتخاب گزینه Mobile Development و انتخاب Xamarin ، آن را نصب و همچنین تمامی فایل‌های مربوط به آن را دریافت کنید. در نظر داشته باشید که در طول این پروسه شما باید از ف.یل.تر شکن استفاده کنید، چرا که بعضی از فایل‌ها مانند Android Sdk بر روی سرور گوگل قرار دارد و گوگل آپی‌های ایران را برای برنامه نویسان فیلتر کرده است (البته اگر بسته‌ی کامل VS 2017 را دریافت کرده باشید، تمام این پیش نیازها، به همراه این بسته‌ی 21~ گیگابایتی موجود است و این مشکلات را نخواهید داشت). 

حال اگر در حال حاضر Visual Studio را بر روی سیستم خود نصب دارید و زمارین را نصب نکرده اید میتوانید از طریق خود سایت زمارین اقدام به دانلود آن بکنید. در نظر داشته باشید، وقتیکه فایل نصبی را از سایت دریافت میکنید، برنامه شروع به نصب خودکار زمارین و تمامی موارد مورد نیاز خود میکند که در این مورد هم به ف.یل.تر شکن نیاز دارید (منهای بسته‌ی کامل VS 2017 که پیشتر عنوان شد). همانطور که بالا گفته شد فایل هایی مانند Android Sdk را نمیتوان از آی پی ایران دانلود کرد (مگر اینکه از سایت‌های واسط استفاده کنید).

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



اگر همه مراحل با موفقیت به پایان برسد، پیغامی مبنی بر موفقیت آمیز بودن عملیات را دریافت خواهید کرد. اما اگر به هر دلیلی فایل نصبی نتواند فایل‌های مورد نیاز را دانلود کند، پیغامی مبنی بر شکست عملیات دانلود فایل مورد نظر را دریافت می‌کنید. در این صورت میتوانید صفحه‌ای را مشاهده کنید که درون آن تمامی فایل‌های دانلود نشده و لینک دانلود مستقیم آنها قرار دارند و شما میتوانید فایل‌های offline آنها را دانلود کرده و به صورت دستی بر روی سیستم نصب نمایید. 


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

 


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

نکته : در نظر داشته باشید فایل‌های زیر برای اجرای برنامه بر روی اندروید میباشند و اگر شما تمایلی به اجرای نرم افزار خود را بر روی اندروید ندارید، میتوانید از آنها صرف نظر کنید.


در این مرحله میخواهیم اولین برنامه زمارین خود را شروع کنیم. همانطور که در مقاله قبلی  توضیح داده شد، شما میتوانید از xamarin forms و یا xamarin native استفاده کنید. Xamarin forms ساختاری برای code sharing میباشد که توسط شرکت زمارین توسعه داده شده‌است. به این ترتیب شما قادر خواهید بود که با استفاده از Xamarin Forms با نوشتن کمترین کد، نرم افزار خود را بر روی پلتفرم‌های پشتیبانی شده‌ی توسط زمارین اجرا کنید. استفاده از Xamarin Forms برای یک پروژه‌ی تجاری بسیار مناسب میباشد؛ اما در برخی موارد شما باید از Xamarin Native هم استفاده نمایید که در این صورت هم زمارین دست شما را نخواهد بست و قادر خواهید بود که به صورت ترکیبی از Xamarin Forms و Xamarin Native از آن استفاده نمایید.


حال به سراغ ساخت اولین پروژه زمارین برویم. در ابتدا برای ایجاد یک پروژه، از منوی File گزینه New و سپس گزینه Project را انتخاب کنید. در پنجره باز شده از سمت چپ گزینه C# را انتخاب و از زیر منوی Cross Platform گزینه Cross Platform App را انتخاب کنید.


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



در این قسمت همانطورکه مشاهده میکنید میتوانید از دو نوع  قالب استفاده کنید و همانطور که از نام آنها مشخص است، Master Detail دو پروژه خواهد ساخت که میتوانید از آن برای سناریو‌های خاص استفاده کنید. در قسمت پایین، UI Technology مشخص کننده‌ی شیوه پیاده سازی ظاهر برنامه است. زمارین فرم برای پیاده سازی ظاهر برنامه از Xaml استفاده میکند و شما میتوانید از طریق آن ظاهر تمامی پلت فرم‌ها را پیاده کنید. همچنین قادر خواهید بود بجای استفاده از Xaml، از زبان‌های مخصوص هر پلتفرم مانند Xml برای اندروید استفاده کرده و ظاهر نرم افزار خود را با استفاده از آن پیاده کنید.


زمارین فرم برای Code Sharing از دو روش استفاده میکند.

Shared Project

مزایا:

· شما به تمامی قسمت‌های یک پلتفرم دسترسی دارید.

· قادر خواهید بود کدهایی مخصوص به هر پلتفرم را بنویسید.

· میتوانید با استفاده از Conditional Compilation، بر اساس نوع پلتفرم، شروطی را اعمال نمایید.

معایب:

· شما با این روش نمیتوانید DLL ایی را تولید کنید و از آن در پروژه‌های دیگر استفاده نمایید.

· نوشتن Unit Testing نسبت به روش Portable پیچیده‌تر است.


Portable Class Library

مزایا:

· شما قادر خواهید بود از پروژه خود DLL تهیه کرده و در مابقی پروژه‌ها از آن استفاده کنید.

· نوشتن Unit Test به سادگی امکان پذیر است.

معایب:

· در این روش شما نمیتوانید از کد‌های مخصوص به هر پلتفرم ( Platform-Specific ) استفاده کنید (البته با استفاده از Dependency Injection امکان پذیر است). 

· فقط قادر خواهید بود که از زیر مجموعه .Net استفاده نمایید.


در ادامه پس از انتخاب Xamarin Forms و Shared Project، بر روی Ok کلیک کنید. همانطور که میبینید ویژوال استودیو شروع به ساخت پروژه‌های برنامه میکند. اگر تمامی Sdk ‌های مورد نیاز را نصب کرده باشید، ویژوال استدیو 5 پروژه را برای شما میسازد (در نظر داشته باشید که اگر SDK مربوط به Universal Windows Platform را هم نصب کرده باشید، پروژه‌ی مربوط به آن را نیز خواهید دید).



اولین پروژه، پروژه Shared است که کد‌های برنامه در این پروژه نوشته خواهد شد.

دومین پروژه همانطور که از پسوند آن مشخص است، پروژه مربوط به اندروید است. در نظر داشته باشید که پروژه‌هایی که مربوط به پلتفرم‌ها میباشند، برای نوشتن کدهای مخصوص به هر پلتفرم در دسترس میباشند و کدهای نوشته شده در این پروژه‌ها و پروژه Shared ترکیب شده و بعد کامپایل میشوند.

در زمارین برنامه‌ها از فایلی به نام App اجرا میشوند. این فایل علاوه بر دسترسی به Life Cycle اپلیکیشن، وظیفه‌ی دیگر آن مشخص کردن اولین صفحه‌ی برنامه نیز میباشد. این فایل درون پروژه Shared  قرار دارد.



همانطور که مشاهده میکنید پس از باز کردن فایل App، در متد سازنده آن صفحه اصلی، به صفحه‌ای به نام MainPage که یک فایل Xaml میباشد، تنظیم شده‌است. پس از باز کردن فایل MainPage با کدهای زیر مواجه میشویم. 

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:PreviewerTest"
             x:Class="PreviewerTest.MainPage">

<Label Text="Welcome to Xamarin Forms!" 
           VerticalOptions="Center" 
           HorizontalOptions="Center" />
</ContentPage>


که مشخص کننده یک لیبل با متن Welcome to Xamarin Forms! میباشد. پس از اجرای برنامه میتوانیم کد اجرا شده را مشاهده کنیم. 

مطالب
پیاده سازی ServiceHostFactory سفارشی در WCF
در این مثال برای اینکه Instance Provider سفارشی خود را بتوانیم به عنوان یک Behavior به سرویس اضافه نماییم باید به خاصیت Description.Behaviors شی ServiceHost دسترسی داشته باشیم. زمانی که در پروژه‌های WCF از روش Self Hosting برای هاست سرویس‌ها استفاده کنیم به دلیل دسترسی مستقیم به شی ServiceHost هر گونه تنظیمات و عملیات Customization به راحتی امکان پذیر است؛ اما در IIS Hosting، از آن جا که به صورت پیش فرض از ServiceHostFactory موجود در WCF استفاده می‌شود ما دسترسی به شی ServiceHost نداریم. برای حل این مسئله باید یک CustomServiceHostFactory ایجاد نماییم که به راحتی در WCF این امکان تدارک دیده شده است.
بررسی یک مثال:
ابتدا کلاسی به صورت زیر ایجاد نمایید. در این کلاس می‌توانید کد‌های لازم برای سفارشی کردن شی ServiceHost را قرار دهید:
public class CustomServiceHost : ServiceHost
{
   public CustomServiceHost( Type t, params Uri baseAddresses ) :
      base( t, baseAddresses ) {}
      
   public override void OnOpening()
   {
       this.Description.Add( new MyServiceBehavior() );
   }
}
اگر از این به بعد به جای استفاده از ServiceHost مستقیما از CustomServiceHost استفاده نماییم، MyServiceBehavior به صورت خودکار به عنوان یک ServiceBehavior برای سرویس مورد نظر در نظر گرفته می‌شود. برای این که هنگام هاست سرویس مورد نظر به صورت خودکار از این شی کلاس استفاده شود می‌توان کلاس Factory ساخت سرویس را تغییر داد به صورت زیر:
public class CustomServiceHostFactory : ServiceHostFactory
{
   public override ServiceHost CreateServiceHost( Type t, Uri[] baseAddresses )
   {
      return new CustomServiceHost( t, baseAddresses )
   }
}
حال بر روی سرویس مورد نظر کلیک راست کرده و گزینه View MarkUp را انتخاب نمایید، چیزی شبیه به گزینه زیر را مشاهده خواهید کرد:
<%@ ServiceHost Language="C#" Debug="true" Service="WcfService1.Service1" CodeBehind="Service1.svc.cs" %>
کافیست کلاس CustomServiceHostFactory را به عنوان Factory این سرویس مشخص نماییم. به صورت زیر:
<%@ ServiceHost Language="C#" Debug="true" Factory="CustomServiceHostFactory" Service="WcfService1.Service1" CodeBehind="Service1.svc.cs" %>

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

مطالب
ارسال PingBack در ASP.NET
Pingback یکی از روش‌های اطلاع رسانی به سایت‌های دیگر در مورد لینک دادن به آن‌ها در سایت خود است. برای مثال من لینکی از یکی از مطالب شما را در متن جاری خودم قرار می‌دهم. سپس به وسیله‌ی ارسال یک ping، در مورد انجام اینکار به شما اطلاع رسانی می‌کنم. حاصل آن عموما قسمت معروف ping-backs سایت‌ها است. این مورد نیز یکی از روش‌های مؤثر SEO در گرفتن backlink است و تبلیغ محتوا.
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام می‌شود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار می‌گیرد؛ بجای استفاده از پروتکل‌های استاندارد وب سرویس‌ها مانند Soap و امثال آن. پیاده سازی‌های ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شده‌است. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامه‌های ASP.NET.


یافتن آدرس وب سرویس سایت پذیرای Pingback

اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه می‌شود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
 <link rel="pingback" href="pingback server">
برای مثال اگر به وبلاگ‌های MSDN دقت کنید، هدر x-pingback را می‌توانید در خروجی وب سرور آن‌ها مشاهده کنید:


همانطور که ملاحظه می‌کنید، نیاز است Response header را آنالیز کنیم.
        private Uri findPingbackServiceUri()
        {
            var request = (HttpWebRequest)WebRequest.Create(_targetUri);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Get;
            request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
            using (var response = request.GetResponse() as HttpWebResponse)
            {
                if (response == null) return null;

                var url = extractPingbackServiceUriFormHeaders(response);
                if (url != null)
                    return url;

                if (!isResponseHtml(response))
                    return null;

                using (var reader = new StreamReader(response.GetResponseStream()))
                {
                    return extractPingbackServiceUriFormPage(reader.ReadToEnd());
                }
            }
        }

        private static Uri extractPingbackServiceUriFormHeaders(WebResponse response)
        {
            var pingUrl = response.Headers.AllKeys.FirstOrDefault(header =>
                                header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) ||
                                header.Equals("pingback", StringComparison.OrdinalIgnoreCase));

            return getValidAbsoluteUri(pingUrl);
        }

        private static Uri extractPingbackServiceUriFormPage(string content)
        {
            if (string.IsNullOrWhiteSpace(content)) return null;
            var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase);
            var match = regex.Match(content);
            return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value);
        }

        private static Uri getValidAbsoluteUri(string url)
        {
            Uri absoluteUri;
            return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri;
        }

        private static bool isResponseHtml(WebResponse response)
        {
            var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header =>
                                        header.Equals("content-type", StringComparison.OrdinalIgnoreCase));
            return !string.IsNullOrWhiteSpace(contentTypeKey) &&
                    response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase);
        }
نحوه‌ی استخراج آدرس سرویس Pingback یک سایت را در کدهای فوق ملاحظه می‌کنید.
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شده‌است. زمانیکه این صفحه را درخواست می‌کنیم، response.Headers.AllKeys حاصل می‌تواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان می‌یابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شده‌است.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شده‌است. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.


ارسال Ping به آدرس سرویس Pingback

اکنون که آدرس سرویس pingback یک سایت را یافته‌ایم، کافی است ping ایی را به آن ارسال کنیم:
        public void Send()
        {
            var pingUrl = findPingbackServiceUri();
            if (pingUrl == null)
                throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host));

            sendPing(pingUrl);
        }

        private void sendPing(Uri pingUrl)
        {
            var request = (HttpWebRequest)WebRequest.Create(pingUrl);
            request.UserAgent = UserAgent;
            request.Timeout = Timeout;
            request.ReadWriteTimeout = Timeout;
            request.Method = WebRequestMethods.Http.Post;
            request.ContentType = "text/xml";
            request.ProtocolVersion = HttpVersion.Version11;
            makeXmlRpcRequest(request);
            using (var response = (HttpWebResponse)request.GetResponse())
            {
                response.Close();
            }
        }

        private void makeXmlRpcRequest(WebRequest request)
        {
            var stream = request.GetRequestStream();
            using (var writer = new XmlTextWriter(stream, Encoding.ASCII))
            {
                writer.WriteStartDocument(true);
                writer.WriteStartElement("methodCall");
                writer.WriteElementString("methodName", "pingback.ping");
                writer.WriteStartElement("params");

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteStartElement("param");
                writer.WriteStartElement("value");
                writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString()));
                writer.WriteEndElement();
                writer.WriteEndElement();

                writer.WriteEndElement();
                writer.WriteEndElement();
            }
        }
اینبار HttpWebRequest تشکیل شده از نوع post است و نه get. همچنین مقداری را که باید ارسال کنیم نیاز است مطابق پروتکل XML-RPC باشد. برای کار با XML-RPC در دات نت یا می‌توان از کتابخانه‌ی Cook Computing's XML-RPC.Net استفاده کرد و یا مطابق کدهای فوق، دستورات آن‌را توسط یک XmlTextWriter کنار هم قرار داد و نهایتا در درخواست Post ارسالی درج کرد.
در اینجا sourceUri آدرس صفحه‌ای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شده‌است. در یک pinback، صرفا این دو آدرس به سرویس دریافت کننده‌ی pingback ارسال می‌شوند.
سپس سایت دریافت کننده‌ی ping، ابتدا sourceUri را دریافت می‌کند تا عنوان آن‌را استخراج کند و همچنین بررسی می‌کند که آیا targetUri، در آن درج شده‌است یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs


نحوه‌ی استفاده از کلاس Pingback تهیه شده

کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش می‌شوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینک‌های خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینک‌های یک محتوا را نیز می‌توان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانه‌ی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url);
var linkTags = doc.DocumentNode.Descendants("link");
var linkedPages = doc.DocumentNode.Descendants("a")
                                  .Select(a => a.GetAttributeValue("href", null))
                                  .Where(u => !String.IsNullOrEmpty(u));
مطالب
Blazor 5x - قسمت 26 - برنامه‌ی Blazor WASM - ایجاد و تنظیمات اولیه
در قسمت قبل، پایه‌ی Web API و سرویس‌های سمت سرور برنامه‌ی کلاینت Blazor WASM این سری را آماده کردیم. این برنامه‌ی سمت کلاینت، قرار است توسط عموم کاربران آن جهت رزرو کردن اتاق‌های هتل فرضی مثال این سری، مورد استفاده قرار گیرد. پیش از این نیز یک برنامه‌ی Blazor Server را تهیه کردیم که کار آن صرفا محدود است به مسائل مدیریتی هتل؛ مانند تعریف اتاق‌ها و امکانات رفاهی آن.


ایجاد یک پروژه‌ی جدید Blazor WASM

برای تکمیل پیاده سازی قسمت سمت کلاینت پروژه‌ی این سری، نیاز به یک پروژه‌ی جدید Blazor WASM را داریم که می‌توان آن‌را با اجرای دستور dotnet new blazorwasm  در یک پوشه‌ی خالی، ایجاد کرد. کدهای این پروژه را می‌توانید در پوشه‌ی HotelManagement\BlazorWasm\BlazorWasm.Client فایل پیوستی انتهای بحث مشاهده کنید.


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

شبیه به کاری که در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» انجام دادیم، در اینجا هم قصد افزودن یکسری کتابخانه‌ی جاوااسکریپتی و CSS ای را داریم که توسط LibMan آن‌ها را مدیریت خواهیم کرد.
- بنابراین در ابتدا به پوشه‌ی BlazorWasm.Client\wwwroot\css وارد شده و پوشه‌های پیش‌فرض bootstrap و open-iconic آن‌را حذف می‌کنیم؛ چون تحت مدیریت هیچ package manager ای نیستند و در این حالت، مدیریت به روز رسانی و یا بازیابی آن‌ها به صورت خودکار میسر نیست.
- سپس فایل wwwroot\css\app.css را هم ویرایش کرده و سطر زیر را از ابتدای آن حذف می‌کنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
- اکنون دستورات زیر را در ریشه‌ی پروژه‌ی WASM، اجرا می‌کنیم تا کتابخانه‌های مدنظر ما، تحت مدیریت libman، در پوشه‌ی wwwroot/lib نصب شوند:
dotnet tool update -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap
libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic
libman install jquery --provider unpkg --destination wwwroot/lib/jquery
libman install toastr --provider unpkg --destination wwwroot/lib/toastr
این دستورات همچنین فایل libman.json متناظری را نیز جهت اجرای دستور libman restore برای دفعات آتی، تولید می‌کند.

- بعد از نصب بسته‌های ذکر شده، فایل wwwroot\index.html را به صورت زیر به روز رسانی می‌کنیم تا به مسیرهای جدید بسته‌های CSS و JS نصب شده، اشاره کند:
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
    />
    <title>BlazorWasm.Client</title>
    <base href="/" />

    <link href="lib/toastr/build/toastr.min.css" rel="stylesheet" />
    <link
      href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css"
      rel="stylesheet"
    />
    <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWasm.Client.styles.css" rel="stylesheet" />
  </head>

  <body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
      An unhandled error has occurred.
      <a href="" class="reload">Reload</a>
      <a class="dismiss">🗙</a>
    </div>

    <script src="lib/jquery/dist/jquery.min.js"></script>
    <script src="lib/toastr/build/toastr.min.js"></script>
    <script src="js/common.js"></script>
    <script src="_framework/blazor.webassembly.js"></script>
  </body>
</html>
مداخل فایل‌های css را در قسمت head و فایل‌های js را پیش از بسته شدن تگ body تعریف می‌کنیم. در اینجا نیازی به ذکر پوشه‌ی آغازین wwwroot نیست؛ چون base href تعریف شده، به این پوشه اشاره می‌کند.

- محتویات فایل wwwroot\css\app.css را هم به صورت زیر تغییر می‌دهیم تا یک spinner و شیوه نامه‌های نمایش تصاویر، به آن اضافه شوند:
.valid.modified:not([type="checkbox"]) {
  outline: 1px solid #26b050;
}

.invalid {
  outline: 1px solid red;
}

.validation-message {
  color: red;
}

#blazor-error-ui {
  background: lightyellow;
  bottom: 0;
  box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
  display: none;
  left: 0;
  padding: 0.6rem 1.25rem 0.7rem 1.25rem;
  position: fixed;
  width: 100%;
  z-index: 1000;
}

#blazor-error-ui .dismiss {
  cursor: pointer;
  position: absolute;
  right: 0.75rem;
  top: 0.5rem;
}

.spinner {
  border: 16px solid silver !important;
  border-top: 16px solid #337ab7 !important;
  border-radius: 50% !important;
  width: 80px !important;
  height: 80px !important;
  animation: spin 700ms linear infinite !important;
  top: 50% !important;
  left: 50% !important;
  transform: translate(-50%, -50%);
  position: absolute !important;
}

@keyframes spin {
  0% {
    transform: rotate(0deg);
  }

  100% {
    transform: rotate(360deg);
  }
}

.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
- همچنین فایل جدید wwwroot\js\common.js را که در قسمت 11 این سری ایجاد کردیم، به پروژه‌ی جاری نیز با محتوای زیر اضافه می‌کنیم تا سبب سهولت دسترسی به toastr شود:
window.ShowToastr = (type, message) => {
  if (type === "success") {
    toastr.success(message, "Operation Successful", { timeOut: 10000 });
  }
  if (type === "error") {
    toastr.error(message, "Operation Failed", { timeOut: 10000 });
  }
};

- در قسمت 11، در بخش «کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی» آن، کلاس JSRuntimeExtensions را تعریف کردیم که سبب کاهش تکرار کدهای استفاده از تابع ShowToastr می‌شود. این فایل‌را در پروژه‌ی BlazorServer.App\Utils\JSRuntimeExtensions.cs این سری نیز استفاده کردیم. یا می‌توان مجددا آن‌را به پروژه‌ی جاری کپی کرد؛ یا آن‌را در یک پروژه‌ی اشتراکی قرار داد. برای مثال اگر آن‌را به پوشه‌ی BlazorWasm.Client\Utils کپی کردیم، نیاز است فضای نام آن‌را اصلاح کرده و سپس آن‌را به انتهای فایل BlazorWasm.Client\_Imports.razor نیز اضافه کنیم تا در تمام کامپوننت‌های برنامه قابل استفاده شود:
@using BlazorWasm.Client.Utils


تغییر و ساده سازی منوی برنامه‌ی کلاینت

در برنامه‌ی کلاینت جاری دیگر نمی‌خواهیم منوی پیش‌فرض سمت چپ صفحه را شاهد باشیم. به همین جهت ابتدا فایل Shared\MainLayout.razor را به صورت زیر ساده می‌کنیم:
@inherits LayoutComponentBase

<NavMenu />
<div>
  @Body
</div>
سپس محتوای فایل Shared\NavMenu.razor را نیز حذف کرده و با تعاریف زیر جایگزین می‌کنیم:
<nav class="navbar navbar-expand-sm navbar-dark bg-dark p-0">
    <a class="navbar-brand mx-4" href="#">Navbar</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent"
            aria-expanded="false"
            aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse pr-2" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto"></ul>
        <ul class="my-0 navbar-nav">
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="registration">
                    <span class="p-2">
                        Register
                    </span>
                </NavLink>
            </li>
            <li class="nav-item p-0">
                <NavLink class="nav-link" href="login">
                    <span class="p-2">
                        Login
                    </span>
                </NavLink>
            </li>
        </ul>
    </div>
</nav>
تا اینجا اگر برنامه‌ی سمت کلاینت را اجرا کنیم، شکل زیر را پیدا کرده که به همراه یک navbar افقی قرار گرفته‌ی در بالای صفحه است؛ به همراه دو لینک به قسمت‌های ثبت‌نام و لاگین:



تغییر محتوای صفحه‌ی آغازین برنامه


صفحه‌ی ابتدایی برنامه، یعنی کامپوننت Pages\Index.razor را نیز به صورت زیر تغییر می‌دهیم:
@page "/"

<form>
    <div class="row p-0 mx-0 mt-4">
        <div class="col-12 col-md-5  offset-md-1 pl-2  pr-2 pr-md-0">
            <div class="form-group">
                <label>Check In Date</label>
                <input type="text" class="form-control" />
            </div>
        </div>
        <div class="col-8 col-md-3 pl-2 pr-2">
            <div class="form-group">
                <label>No. of nights</label>
                <select class="form-control">
                    @for (var i = 1; i <= 10; i++)
                    {
                        <option value="@i">@i</option>
                    }
                </select>
            </div>
        </div>
        <div class="col-4 col-md-2 p-0 pr-2">
            <div class="form-group">
                <label>&nbsp;</label>
                <input type="submit" value="Go" class="btn btn-success btn-block" />
            </div>
        </div>
    </div>
</form>
در اینجا فرمی تعریف شده که تاریخ ورود و رزرو اتاقی را مشخص می‌کند؛ به همراه دراپ‌داونی برای انتخاب تعداد شب‌های اقامت مدنظر.


تعریف View Model رابط کاربری Pages\Index.razor

پس از تعریف محتوای ثابت برنامه، اکنون نوبت به پویا سازی آن است. به همین جهت نیاز است مدلی را برای صفحه‌ی آغازین برنامه تعریف کرد تا بتوان فرم آن‌را به این مدل متصل کرد. این مدل چون مختص به برنامه‌ی کلاینت است، آن‌را در پوشه‌ی جدید Models\ViewModels ایجاد می‌کنیم:
using System;

namespace BlazorWasm.Client.Models.ViewModels
{
    public class HomeVM
    {
        public DateTime StartDate { get; set; } = DateTime.Now;

        public DateTime EndDate { get; set; }

        public int NoOfNights { get; set; } = 1;
    }
}
در اینجا EndDate، یک خاصیت محاسباتی است که بر اساس تاریخ شروع و تعداد شب‌های انتخابی، قابل محاسبه‌است.
پس از این تعریف، بهتر است فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor افزود، تا کار با آن در کامپوننت‌های برنامه، ساده‌تر شود:
using BlazorWasm.Client.Models.ViewModels
اکنون می‌توان فرم Pages\Index.razor را به مدل فوق متصل کرد که شامل این تغییرات است:
- ابتدا فیلدی که ارائه کننده‌ی شیء ViewModel فرم است را تعریف می‌کنیم:
@code{
    HomeVM HomeModel = new HomeVM();
}
- سپس بجای یک form ساده، از EditForm اشاره کننده‌ی به این فیلد، استفاده خواهیم کرد:
<EditForm Model="HomeModel">
 // ...
</EditForm>
- در آخر بجای input معمولی، از کامپوننت InputDate متصل به HomeModel.StartDate :
<InputDate min="@DateTime.Now.ToString("yyyy-MM-dd")"
           @bind-Value="HomeModel.StartDate"
           type="text"
           class="form-control" />
و بجای select معمولی، از نمونه‌ی متصل شده‌ی به HomeModel.NoOfNights استفاده می‌کنیم:
<select @bind="HomeModel.NoOfNights">


تعریف Local Storage سمت کلاینت

در ادامه می‌خواهیم اگر کاربری زمان شروع رزرو اتاقی را به همراه تعداد شب مدنظر، انتخاب کرد، با کلیک بر روی دکمه‌ی Go، به یک صفحه‌ی مشاهده‌ی جزئیات منتقل شود. بنابراین نیاز داریم تا اطلاعات انتخابی کاربر را به نحوی ذخیره سازی کنیم. برای یک چنین سناریوی سمت کلاینتی، می‌توان از local storage استاندارد مرورگرها استفاده کرد که امکان کار آفلاین با برنامه را نیز فراهم می‌کند.
برای این منظور کتابخانه‌ای به نام Blazored.LocalStorage طراحی شده‌است که پس از نصب آن توسط دستور زیر:
dotnet add package Blazored.LocalStorage
نیاز است سرویس‌های آن‌را به سیستم تزریق وابستگی‌های برنامه اضافه کرد. در برنامه‌های Blazor Server، اینکار را در فایل Startup برنامه انجام می‌دادیم؛ اما در اینجا، سرویس‌ها در فایل Program.cs تعریف می‌شوند:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...
            builder.Services.AddBlazoredLocalStorage();
            // ...
        }
    }
}
پس از این تعاریف می‌توان از سرویس ILocalStorageService آن در کامپوننت‌های برنامه استفاده کرد. البته جهت سهولت استفاده‌ی از این سرویس بهتر است فضای نام آن‌را به فایل BlazorWasm.Client\_Imports.razor افزود:
@using Blazored.LocalStorage
اکنون برای استفاده از آن به کامپوننت Pages\Index.razor مراجعه کرده و سرویس‌های ILocalStorageService و IJSRuntime را به کامپوننت تزریق می‌کنیم:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime

<EditForm Model="HomeModel" OnValidSubmit="SaveInitialData">
همچنین متدی را هم برای مدیریت رویداد OnValidSubmit تعریف خواهیم کرد:
@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync("InitialRoomBookingInfo", HomeModel);
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}
در اینجا با استفاده از متد SetItemAsync و ذکر یک کلید دلخواه، اطلاعات مدل فرم را در local storage مرورگر ذخیره کرده‌ایم. همچنین اگر خطایی هم رخ دهد توسط ToastrError نمایش داده خواهد شد.
برای مثال اگر تاریخ و عددی را انتخاب کنیم، نتیجه‌ی حاصل از کلیک بر روی دکمه‌ی Go را می‌توان در قسمت Local storage مرورگر جاری مشاهده کرد:


البته با توجه به اینکه می‌خواهیم از کلید InitialRoomBookingInfo در سایر کامپوننت‌های برنامه نیز استفاده کنیم، بهتر است آن‌را به یک پروژه‌ی مشترک مانند BlazorServer.Common که پیشتر نام نقش‌هایی مانند Admin را در آن تعریف کردیم، منتقل کنیم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        public const string LocalInitialBooking = "InitialRoomBookingInfo";
    }
}
سپس باید ارجاعی به آن پروژه را افزوده:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <ProjectReference Include="..\..\BlazorServer\BlazorServer.Common\BlazorServer.Common.csproj" />
  </ItemGroup>
</Project>
همچنین فضای نام آن‌را نیز به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@using BlazorServer.Common
اکنون می‌توان از کلید ثابت تعریف شده‌ی مشترک، استفاده کرد:
await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);

در آخر قصد داریم با کلیک بر روی Go، به یک صفحه‌ی جدید مانند نمایش لیست اتاق‌ها هدایت شویم. به همین جهت کامپوننت جدید Pages\HotelRooms\HotelRooms.razor را ایجاد می‌کنیم:
@page "/hotel/rooms"

<h3>HotelRooms</h3>

@code {

}
سپس در کامپوننت Pages\Index.razor با استفاده از سرویس NavigationManager، کار هدایت خودکار کاربر را به این کامپوننت جدید انجام خواهیم داد:
@page "/"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager


@code{
    HomeVM HomeModel = new HomeVM();

    private async Task SaveInitialData()
    {
        try
        {
            HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights);
            await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);
            NavigationManager.NavigateTo("hotel/rooms");
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-26.zip