نظرات مطالب
C# 7 - Tuple return types and deconstruction
مزیت خروجی‌های از نوع Tuple برای طراحان کتابخانه‌ها 
تبدیل خروجی‌های dynamic قبلی
// query dapper via "dynamic"
int id = ...
var row = conn.QuerySingle("select Name, LastUpdated from Records where Id=@id", new {id});
string name = row.Name; // "dynamic"
به tuple جدید
// query dapper via C# 7 tuples
int id = ...
var row = conn.QuerySingle<(string name, DateTime lastUpdated)>(
     "select Name, LastUpdated from Records where Id=@id", new {id});
// use row.name and row.lastUpdated
با مزیت نداشتن هیچ نوع سربار اضافه‌ای (کد dynamic ابتدایی جهت پردازش، DLR را وارد صحنه می‌کند؛ اما کد فوق خیر. به علاوه چون value type است، حتی سربار heap allocation را هم ندارد).
مطالب
آشنایی با Oslo - قسمت دوم

قبل شروع این قسمت بد نیست با یک سری از وبلاگ‌های اعضای تیم Oslo آشنا شویم:


در ادامه‌ی مثال قسمت قبل، اکنون می‌خواهیم entity جدیدی به نام Project را به مدل اضافه کنیم:

//mschema to define a Project type
type Project
{
ProjectID : Integer64 = AutoNumber();
ProjectName : Text#25;
ConectionStringSource : Text;
ConectionStringDestination : Text;
DateCompared: DateTime;
Comment: Text?;
ProjectOwner: ApplicationUser;
} where identity ProjectID;

مطابق تعاریف فوق، فیلد ProjectOwner ارجاعی را به نوع ApplicationUser که پیشتر ایجاد کردیم دارد. اکنون برای مشاهده‌ی تغییرات حاصل شده نیاز به ایجاد یک جدول از روی این نوع جدید است که foreign key آن به صورت زیر تعریف می‌شود:

//this will define a SQL foreign key relationship
ProjectCollection : Project* where item.ProjectOwner in ApplicationUserCollection;

پس از افزودن این سطر، Intellipad بلافاصله اسکریپت T-SQL آن‌را برای ما ایجاد می‌کند که به شرح زیر است:

set xact_abort on;
go

begin transaction;
go

set ansi_nulls on;
go

create schema [Test1];
go

create table [Test1].[ApplicationUserCollection]
(
[UserID] bigint not null identity,
[FirstName] nvarchar(max) null,
[LastName] nvarchar(25) not null,
[Password] nvarchar(10) not null,
constraint [PK_ApplicationUserCollection] primary key clustered ([UserID])
);
go

create table [Test1].[ProjectCollection]
(
[ProjectID] bigint not null identity,
[Comment] nvarchar(max) null,
[ConectionStringDestination] nvarchar(max) not null,
[ConectionStringSource] nvarchar(max) not null,
[DateCompared] datetime2 not null,
[ProjectName] nvarchar(25) not null,
[ProjectOwner] bigint not null,
constraint [PK_ProjectCollection] primary key clustered ([ProjectID]),
constraint [FK_ProjectCollection_ProjectOwner_Test1_ApplicationUserCollection] foreign key ([ProjectOwner]) references [Test1].[ApplicationUserCollection] ([UserID])
);
go

insert into [Test1].[ApplicationUserCollection] ([FirstName], [LastName], [Password])
values (N'user1', N'name1', N'1@34')
;

insert into [Test1].[ApplicationUserCollection] ([FirstName], [LastName], [Password])
values (N'user2', N'name2', N'123@4')
;

insert into [Test1].[ApplicationUserCollection] ([FirstName], [LastName], [Password])
values (N'user3', N'name3', N'56#2')
;

insert into [Test1].[ApplicationUserCollection] ([FirstName], [LastName], [Password])
values (N'user4', N'name4', N'789@5')
;
go

commit transaction;

Go

همانطور که ملاحظه‌ می‌کنید، هنگام کار کردن با یک مدل، نگهداری و توسعه‌ی آن واقعا ساده‌تر است از ایجاد این دستورات T-SQL .

نکته:
جهت آشنایی با انواع داده‌های مجاز در زبان M می‌توان به مستندات رسمی آن مراجعه نمود:
The "Oslo" Modeling Language Specification

اکنون قصد داریم همانند مثال قسمت قبل، تعدادی رکورد آزمایشی را برای این جدول تعریف کنیم:

ProjectCollection
{
Project1{
ProjectName = "My Project 1",
ConectionStringSource = "Data Source=.;Initial Catalog=MyDB1;Integrated Security=True;",
ConectionStringDestination = "Data Source=.;Initial Catalog=MyDB2;Integrated Security=True;",
Comment="Project Comment",
DateCompared=2009-01-01T00:00:00,
ProjectOwner=ApplicationUserCollection.User1 //direct ref to User1 (FK)
},
Project2{
ProjectName = "My Project 2",
ConectionStringSource = "Data Source=.;Initial Catalog=MyDB1;Integrated Security=True;",
ConectionStringDestination = "Data Source=.;Initial Catalog=MyDB2;Integrated Security=True;",
Comment="Project Comment",
DateCompared=2009-01-01T00:00:00,
ProjectOwner=ApplicationUserCollection.User2 //direct ref to User2 (FK)
}

}

چون بین ProjectOwner و ApplicationUserCollection رابطه ایجاد کرده‌ایم، هنگام استفاده از آن‌ها، برنامه Intellipad جهت سهولت کار، IntelliSense مربوطه را نیز نمایش خواهد داد :


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

مطالب
React 16x - قسمت 26 - احراز هویت و اعتبارسنجی کاربران - بخش 1 - ثبت نام و ورود به سیستم
می‌خواهیم به برنامه‌ی لیست فیلم‌هایی که تا این قسمت تکمیل کردیم، امکانات جدیدی را مانند ورود به سیستم، خروج از آن، کار با JWT، فراخوانی منابع محافظت شده‌ی سمت سرور، نمایش و یا مخفی کردن المان‌های صفحه بر اساس سطوح دسترسی کاربر و همچنین محافظت از مسیرهای مختلف تعریف شده‌ی در برنامه، اضافه کنیم.
برای قسمت backend، از همان برنامه‌ی تکمیل شده‌ی قسمت قبل استفاده می‌کنیم که به آن تولید مقدماتی JWTها نیز اضافه شده‌است. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید می‌کنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روش‌های پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
  1. «معرفی JSON Web Token»
  2. «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» 
  3. «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
  4. « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»  


ثبت یک کاربر جدید

فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
{
  "name": "string",
  "email": "string",
  "password": "string",
  "isAdmin": true,
  "id": 0
}
در سمت سرور هم در Services\UsersDataSource.cs که در انتهای بحث می‌توانید پروژه‌ی کامل آن‌را دریافت کنید، منحصربفرد بودن ایمیل وارد شده بررسی می‌شود و اگر یک رکورد دو بار ثبت شود، یک BadRequest را به همراه پیام خطایی، بازگشت می‌دهد.

اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpoint‌های backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش می‌گیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامه‌ی frontend اضافه می‌کنیم:
import http from "./httpService";
import { apiUrl } from "../config.json";

const apiEndpoint = apiUrl + "/users";

export function register(user) {
  return http.post(apiEndpoint, {
    email: user.username,
    password: user.password,
    name: user.name
  });
}
توسط متد register این سرویس می‌توانیم شیء user را با سه خاصیت مشخص شده، از طریق HTTP Post، به آدرس api/Users ارسال کنیم. خروجی این متد نیز یک Promise است. در این سرویس، تمام متدهایی که قرار است با این endpoint سمت سرور کار کنند، مانند ثبت، حذف، دریافت اطلاعات و غیره، تعریف خواهند شد.
اطلاعات شیء user ای که در اینجا دریافت می‌شود، از خاصیت data کامپوننت RegisterForm تامین می‌گردد:
class RegisterForm extends Form {
  state = {
    data: { username: "", password: "", name: "" },
    errors: {}
  };
البته اگر دقت کرده باشید، در شیء منتسب به خاصیت data، خاصیتی به نام username تعریف شده‌است، اما در سمت سرور، نیاز است خاصیتی با نام Name را دریافت کنیم. یک چنین نگاشتی در داخل متد register سرویس کاربر، قابل مشاهده‌‌است. در غیراینصورت می‌شد در متد http.post، کل شیء user را به عنوان پارامتر دوم، درنظر گرفت و ارسال کرد.

پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آن‌را import می‌کنیم:
import * as userService from "../services/userService";
می‌شد این سطر را به صورت زیر نیز نوشت، تا تنها یک متد از ماژول userService را دریافت کنیم:
import { register } userService from "../services/userService";
اما روش as userService * به معنای import تمام متدهای این ماژول است. به این ترتیب نام ذکر شده‌ی پس از as، به عنوان شیءای که می‌توان توسط آن به این متدها دسترسی یافت، قابل استفاده می‌شود؛ مانند فراخوانی متد userService.register. اکنون می‌توان متد doSubmit این فرم را به سرور متصل کرد:
  doSubmit = async () => {
    try {
      await userService.register(this.state.data);
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };


مدیریت و نمایش خطاهای دریافتی از سمت سرور

در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور می‌توان شاهد بود که id جدیدی را به این رکورد نسبت داده‌است:


اگر مجددا همین رکورد را به سمت سرور ارسال کنیم، اینبار خطای زیر را دریافت خواهیم کرد:


که از نوع 400 یا همان BadRequest است:


بنابراین نیاز است بدنه‌ی response را در یک چنین مواردی که خطایی از سمت سرور صادر می‌شود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنه‌ی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:


پیشتر در قسمت بررسی «کار با فرم‌ها» آموختیم که برای مدیریت خطاهای پیش بینی شده‌ی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنه‌ی try/catch‌ها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت می‌دهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنه‌ی catch نوشته شده ملاحظه می‌کنید.
سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط می‌شود، با پیام دریافت شده‌ی از سمت سرور، مقدار دهی کنیم که در اینجا می‌دانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنه‌ی response را به خاصیت جدید errors.username انجام می‌دهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده می‌شود.


پیاده سازی ورود به سیستم

فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
{
  "email": "string",
  "password": "string"
}
با ارسال این اطلاعات به سمت سرور، درخواست Login انجام می‌شود. سرور نیز در صورت تعیین اعتبار موفقیت آمیز کاربر، به صورت زیر، یک JSON Web token را بازگشت می‌دهد:
var jwt = _tokenFactoryService.CreateAccessToken(user);
return Ok(new { access_token = jwt });
یعنی بدنه‌ی response رسیده‌ی از سمت سرور، دارای یک شیء JSON خواهد بود که خاصیت access_token آن، حاوی JSON Web token متعلق به کاربر جاری لاگین شده‌است. در آینده اگر این کاربر نیاز به دسترسی به یک api endpoint محافظت شده‌ای را در سمت سرور داشته باشد، باید این token را نیز به همراه درخواست خود ارسال کند تا پس از تعیین اعتبار آن توسط سرور، مجوز دسترسی به منبع درخواستی برای او صادر شود.

در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آن‌را در فایل جدید src\services\authService.js، با محتوای زیر ایجاد می‌کنیم:
import { apiUrl } from "../config.json";
import http from "./httpService";

const apiEndpoint = apiUrl + "/auth";

export function login(email, password) {
  return http.post(apiEndpoint + "/login", { email, password });
}
متد login، کار ارسال ایمیل و کلمه‌ی عبور کاربر را به اکشن متد Login کنترلر Auth، انجام می‌دهد و خروجی آن یک Promise است. برای استفاده‌ی از آن به کامپوننت src\components\loginForm.jsx بازگشته و متد doSubmit آن‌را به صورت زیر تکمیل می‌کنیم:
import * as auth from "../services/authService";

class LoginForm extends Form {
  state = {
    data: { username: "", password: "" },
    errors: {}
  };

  // ...

  doSubmit = async () => {
    try {
      const { data } = this.state;
      const {
        data: { access_token }
      } = await auth.login(data.username, data.password);
      console.log("JWT", access_token);
      localStorage.setItem("token", access_token);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors };
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };
توضیحات:
- ابتدا تمام خروجی‌های ماژول authService را با نام شیء auth دریافت کرده‌ایم.
- سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کرده‌ایم. این متد چون یک Promise را باز می‌گرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
- همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شده‌است.
- سپس نیاز است برای استفاده‌های آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکان‌های متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
- در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحه‌ی اصلی آن هدایت می‌کنیم.
- در اینجا قسمت catch نیز ذکر شده‌است تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.

اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحه‌ی اصلی برنامه هدایت می‌شود. همچنین این token ذخیره شده را می‌توان در ذیل قسمت application->storage آن نیز مشاهده کرد:



لاگین خودکار کاربر، پس از ثبت نام در سایت

پس از ثبت نام یک کاربر در سایت، بدنه‌ی response بازگشت داده شده‌ی از سمت سرور، همان شیء user است که اکنون Id او مشخص شده‌است. بنابراین اینبار جهت ارائه‌ی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده می‌کنیم (کدهای کامل آن در انتهای بحث پیوست شده‌است):
var jwt = _tokenFactoryService.CreateAccessToken(user);
this.Response.Headers.Add("x-auth-token", jwt);



در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
const response = await userService.register(this.state.data);
console.log(response);
یک چنین خروجی را به همراه دارد که در آن، هدر سفارشی ما درج نشده‌است و فقط هدر content-type در آن مشخص است:


برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
var jwt = _tokenFactoryService.CreateAccessToken(data);
this.Response.Headers.Add("x-auth-token", jwt);
this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
به این ترتیب وب سرور برنامه، هدر سفارشی را که قرار است برنامه‌ی کلاینت به آن دسترسی پیدا کند، مجاز اعلام می‌کند. اینبار اگر خروجی دریافتی از Axios را لاگ کنیم، در لیست هدرهای آن، هدر سفارشی x-auth-token نیز ظاهر می‌شود:


اکنون می‌توان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشه‌ی سایت هدایت نمود:
class RegisterForm extends Form {
  // ...

  doSubmit = async () => {
    try {
      const response = await userService.register(this.state.data);
      console.log(response);
      localStorage.setItem("token", response.headers["x-auth-token"]);
      this.props.history.push("/");
    } catch (ex) {
      if (ex.response && ex.response.status === 400) {
        const errors = { ...this.state.errors }; // clone an object
        errors.username = ex.response.data;
        this.setState({ errors });
      }
    }
  };

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip
مطالب
نکاتی توصیه ای برای برنامه نویسی اندروید : قسمت اول
اگر جدیدا قصد برنامه نویسی اندروید را کرده‌اید، یا هنوز روش‌های متدوالی را برای کار با این زبان انتخاب نکرده‌اید؛ به نظرم این مقاله می‌تواند کمک خوبی برای شما باشد. مسائلی که بیان میکنم در واقع از تجربیات شخصی و راه حل هایی است که برای خودم تعیین کرده‌ام و تعدادی از آن‌ها را در طول مدتی که در این زمینه فعالیت کرده‌ام، از جاهای مختلف دیده و در یک جا گردآوری کرده‌ام. برای نامگذاری اشیاء و متغیرها و دیگر موارد، من از این قاعده پیروی میکنم که به نظرم بسیار ایده آل می‌باشد. الگوی معماری هم که جدیدا مورد استفاده قرار داده‌ام، الگوی MVP است که نمونه‌ای از آن، در گیت هاب قرار گرفته است. البته این مثال ساده تر نیز وجود دارد. تشریح کامل این معماری را به همراه آزمون واحد آن، می‌توانید در این مقاله سه قسمتی ببینید.

در اینجا، یک سری نکات را در طول برنامه نویسی، متذکر می‌شوم تا مدیریت کدهای شما را در اندروید راحت‌تر کند.

یک نکته‌ی دیگر را که باید متذکر شوم این است که همه اصطلاحاتی که در این مقاله استفاده می‌شوند بر اساس اندروید استادیو و مستندات رسمی گوگل است است؛ به عنوان نمونه عبارت‌های ماژول و پروژه آن چیزی هستند که ما در اندروید استادیو به آن‌ها اشاره می‌کنیم، نه آنچه که کاربران Eclipse به آن اشاره می‌کنند.

یک. برای هر تکه کد و یا متدی که می‌نویسید مستندات کافی قرار دهید و اگر این متد نیاز به مجوز خاصی دارد مانند نمونه زیر، آن را حتما ذکر کنید:
/**
     *
     * <p>
     *   check network is available or not <br/>
     *   internet connection is not matter,for check internet connection refer to IsInternetConnected() Method in this class
     * </p>
       * <p>
     *   Required Permission : <b>android.permission.ACCESS_NETWORK_STATE</b>
     * </p>
     * @param context
     * @return returns true if a network is available
     */
    public boolean isNetworkAvailable(Context context) {

        ConnectivityManager connectivityManager
                = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo();
        return activeNetworkInfo != null && activeNetworkInfo.isConnected();
    }
همچنین اگر، مورد خاص دیگری مثل بالا بود، حتما آن را ذکر کنید. می‌توانید از تگ گذاری در کامنت ها نیز استفاده کنید. از ویژگی‌های کامنت todo در اندروید استادیو این است که میتوانید در حین کار با سیستم گیت نیز از آن بهره ببرید  و قبل از کامیت کردن کد، کدهای todo به شما یادآوری شوند و هر پیکربندی را که لازم دارید، روی آن انجام دهید.



دو. از یک کلاس واحد جهت استفاده از اطلاعات عمومی و یا ثابت‌ها استفاده نمایید. این اطلاعات می‌توانند شامل: مسیرها، آدرس‌های وب سرویس، شماره اختصاصی هر نوتیفیکیشن و .... باشند. برای اینکار میتوان هر کدام از اطلاعات را داخل یک کلاس قرار داد و همه این کلاس‌ها را به صورت استاتیک تعریف کنید تا بدین شکل در دسترس قرار بگیرند (از الگوی singleton هم می‌توان استفاده کرد).
public class ProjectSettings
{
       public static NotificationsId=new NotificationsId();
       public static UrlAddresss=new UrlAddresss();
       public static SdPath=new SdPath();
       ......
}
نحوه صدا زدن هم به همین شکل می‌شود:
ProjectSettings.NotificationsId.UpdateNotificationId
بدین شکل هم به طور ساده و مفهومی صدا زده می‌شود و هم اینکه در همه جای برنامه این ثابت‌ها و مقادیر قابل استفاده هستند. به عنوان مثال به شماره هر نوتیفیکیشن از همه جا دسترسی دارید و هم اینکه شماره‌ای تکراری اشتباها انتخاب نمی‌شود.

سه. حداکثر استفاده از اینترفیس را به خصوص برای UI انجام بدهید:
به عنوان نمونه، بسیاری نمایش یک toast را به شکل زیر انجام می‌دهند:
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
یا اینکه برای یک دیالوگ مستقیما و در جا همانجا به کدنویسی مشغول می‌شوند. این روش‌ها هیچ مشکلی ندارند ولی در آینده نگهداری کد را مشکل می‌کنند. مثلا تصور کنید شما بسیاری از جاهای برنامه، Toast زدید و حالا قصد دارید در نسخه بعدی برنامه، toast‌های دلخواه و یا custom ایی را ایجاد کنید. در این صورت مجبورید کل برنامه را رصد کرده و هر جا toast هست آن را تغییر دهید. در اینجا هم اصول DRY را نادیده گرفته‌اید و هم زحمت شما زیاد شده‌است و حتی ممکن است یک یا چندتایی از قلم بیفتند. برای دیالوگ‌ها هم بدین صورت خواهد بود و خیلی از مسائل دیگر. به همین جهت استفاده از اینترفیس‌ها توصیه می‌شود و فردا نیز اگر باز یک کلاس دیگر را نوشتید، خیلی راحت آن را با کلاس فعلی تعویض می‌کنید.
public interface IMessageUI
{
    void ShowToast(Context context,String message);
}

public class MessageUI impelement IMessageUI
{
      public void ShowToast(Context context,string message)
       {
              Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
       }
}

چهار
. اگر برای اولین بار است وارد اندروید می‌شوید، خوب چرخه‌های یک شیء، چون اکتیویتی یا فراگمنت را یاد بگیرید تا در آینده با مشکلات خاصی روبرو نشوید.
به عنوان مثال درست است که اولین رویداد فراخوانی در onCreate رخ میدهد ولی همیشه محل مناسبی برای دریافت دیتاها در زمان اولیه نیست. به عنوان مثال تصور کنید که لیستی در اکتیویتی A دارید و به اکتیویتی B می‌روید و یک آیتم به اطلاعات اضافه می‌شود و موقعی که به اکتیویتی A بر می‌گردید، زیاد تعجب نکنید که لیست دقیقا به همان شکل قبلی است و خبری از آیتم جدید نیست.
  چون اکتیویتی در حالت stop بوده و بعد از آن به حالت Resume رفته و تا موقعی که این اکتیویتی از حافظه خارج نشود یا گوشی چرخش نداشته باشد، واکشی دیتاها صورت نخواهد گرفت. پس بهترین مکان در این حالت، رویداد OnStart است که در هر دو وضعیت صدا زده می‌شود؛ یا اینکه در OnRestatr روی آداپتور تغییرات جدید را اعمال کنید تا نیازی به واکشی مجدد داده‌ها نباشد. 



به طور خلاصه نحوه اجرای رویدادها بدین شکل است که ابتدای رویداد OnCreate اجرا می‌شود که هنوز هیچ UI ئی در آن پیاده سازی نشده‌است و شما در اینجا موظفید Layout خود را معرفی کنید. رویداد OnStart بعد از آن موقعی که UI آماده شده است، اجرا می‌گردد. سپس رویداد OnResume اجرا می‌شود.

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

از رویداد Onresume میتوانید برای کارهایی که بین زمان آغاز اکتیویتی و برگشت اکتیویتی مشترکند استفاده کرد. اگر به هر نحوی اکتیویتی به طور کامل پنهان شود٬، به این معناست که شما به اکتیویتی دیگری رفته‌اید رویداد OnStop اجرا شده‌است و در صورت بازگشت، رویداد OnRestart اجرا خواهد شد. ولی اگر مدت طولانی از رویداد OnStop بگذرد احتمال اینکه سیستم مدیریت منابع اندروید، اکتیویتی شما را از حافظه خارج کند زیاد است و رویداد OnDestroy صورت خواهد گرفت. در این حالت دفعه بعد، مجددا همه عملیات از ابتدا آغاز می‌گردند.

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

شش. اگر برنامه شما قرار است در چندین حالت مختلفی که اتفاق می‌افتد، یک کار خاصی را انجام دهد، برای برنامه‌تان یک Receiver بنویسید و در آن کدهای تکراری را نوشته و در محل‌های مختلف وقوع آن رویدادها، رسیور را صدا بزنید. برای نمونه برنامه تلگرام یک سرویس پیام رسان پشت صحنه دارد که در دو رویداد قرار است اجرا شوند. یکی موقعی که گوشی بوت خود را تکمیل کرده است و در حال آغاز فرایندهای سیستم عامل است و دیگر زمانی است که برنامه اجرا می‌شود. در اینجا تلگرام از یک رسیور سیستمی برای آگاهی از بوت شدن و یک رسیور داخل برنامه جهت آگاهی از اجرای برنامه استفاده میکند و هر دو به یک کلاس از جنس BroadcastReceiver متصلند:
      <receiver android:name=".AppStartReceiver" android:enabled="true">
            <intent-filter>
                <action android:name="org.telegram.start" />
                <action android:name="android.intent.action.BOOT_COMPLETED" />
            </intent-filter>
        </receiver>

public class AppStartReceiver extends BroadcastReceiver {
    public void onReceive(Context context, Intent intent) {
        AndroidUtilities.runOnUIThread(new Runnable() {
            @Override
            public void run() {
                ApplicationLoader.startPushService();
            }
        });
    }
}
برای نام رسیورهای داخلی هم میتوانید مورد شماره 2 را اجرا کنید.
برنامه تلگرام حتی برای حالت‌های پخش هم رسیورها استفاده کرده است که در همین رسیور وضعیت تغییر پلیر مشخص می‌شود:
    <receiver android:name=".MusicPlayerReceiver" >
            <intent-filter>
                <action android:name="org.telegram.android.musicplayer.close" />
                <action android:name="org.telegram.android.musicplayer.pause" />
                <action android:name="org.telegram.android.musicplayer.next" />
                <action android:name="org.telegram.android.musicplayer.play" />
                <action android:name="org.telegram.android.musicplayer.previous" />
                <action android:name="android.intent.action.MEDIA_BUTTON" />
                <action android:name="android.media.AUDIO_BECOMING_NOISY" />
            </intent-filter>
        </receiver>
اینگونه تلگرام میتواند از همه جا سرویس را کنترل کند. مثلا موقعی که دانلود یک موزیک تمام شده، سریعا پخش آن موزیک دانلود شده را آغاز کند.

هفت. اگر از یک ORM برای لایه داده‌ها استفاده می‌کنید (قبلا در سایت جاری در مورد ORM‌های اندروید صحبت کرده‌ایم و  ORM‌های خوش دستی که خودم از آن‌ها استفاده میکنم ActiveAndroid و CPORM هستند که هم کار کردن با آنها راحت است و هم اینکه امکانات خوبی را عرضه می‌کنند) در این نوع ORM‌ها شما نباید انتظار چیزی مانند EF را داشته باشید و در بعضی موارد باید کمی خودتان کمک کنید. به عنوان مثال در Active Android برای ایجاد یک inner join باید به شکل زیر بنویسید:
     From query= new Select()
               .from(Poem.class)
               .innerJoin(BankPoemsGroups.class)
               .on("poems.id=bank_poems_groups.poem")
               .where("BankGroup=?", String.valueOf(groupId));
        return query.execute();
همانطور که می‌بینید بخش‌هایی از آن مثل جوین‌ها و شرط‌ها را باید خودتان تکمیل کنید. از آنجا که ممکن است در آینده نام فیلد تغییر کند یا اینکه در حین انبوهی از کدها، عبارت رشته‌ای را اشتباه وارد کنید، بهتر است به این فرم کار کنید:
@Table(name="poems")
public class Poem extends Model {

    public static String tableName="poems";
    public static  String codeColumn="code";
    public static  String titleColumn="title";
    public static  String bookColumn="book";
    ......

    @Column(name="code",index = true)
    public int Code;

    @Column(name="title")
    public String Title;

    @Column(name="book")
    public Book Book;
.....}
در مدل بالا، نام فیلدها و جداول به صورت استاتیک تعریف شده‌اند. حالا می‌توانیم از این اسامی به راحتی در لایه سرویس استفاده کنیم:
    From query= new Select()
               .from(Poem.class)
               .innerJoin(BankPoemsGroups.class)
               .on(Poem.TableName+"."+ Poem.IdColumn+"="+ BankPoemsGroups.TableName+"."+ BankPoemsGroups.PoemColumn)
               .where(Poem.BankGroupColumn+"=?", String.valueOf(groupId));
        return query.execute();
حالا کمی بهتر شد. هم برای تغییر آینده بهتر شد و هم اینکه احتمال خطای تایپی کاهش یافت. ولی باز هم ایجاد کوئری هنوز سخت است و نوشتن مرتب یک رابطه جوین و شرطی و چسباندن مداوم رشته‌ها کار خسته کننده‌ای است و احتمال خطای سهوی و انسانی هم در آن بالاست. برای رفع این مشکل بهتر است یک کلاس جدید برای ساخت این کوئری‌ها داشته باشیم که یک نمونه از آن را در این پایین می‌بینید:
public class QueryConcater {
  public String GetInnerJoinQuery(String table1,String field1,String table2,String field2)
    {
        String query=table1 +"." +field1+"="+table2+"."+field2;
        return query;
    }
......
}
در ادامه برای مرتب سازی و شرط و ... هم می‌نویسیم:
   return new Select()
                .from(Color.class)
                .innerJoin(ProductItem.class)
                .on(queryConcater.GetInnerJoinQuery(ProductItem.TableName,
                        ProductItem.ColorColumn, Color.TableName))
                .where(queryConcater.WhereConditionQuery
                        (ProductItem.TableName, ProductItem.ProductColumn), productId)
                .execute();
در دستورات بالا از این کلاس دو متد برای کوئری جوین و یکی هم برای ساخت شرط ایجاد شده است و مقادیر به صورت پارامتر داده شده‌اند. این الگو کمک میکند که اگر هم این تکه کد اشتباه باشد، با تغییر یکجا بقیه کدها هم تغییر میکنند و اگر در آینده هم ORM تغییر یافت، نحوه کوئری نویسی‌ها در این کلاس تغییر کنند، نه اینکه در طول لایه سرویس پراکنده باشند.

هشت. سعی کنید همیشه از یک سیستم گزارش خطا در اپلیکیشن خود استفاده کنید. در حال حاضر معروفترین سیستم گزارش خطا Acra است که می‌توانید backend آن را هم از اینجا تهیه کنید و اگر هم نخواستید، سایت Tracepot امکانات خوبی را به رایگان برای شما فراهم می‌کند. از این پس با سیستم آکرا شما به یک سیستم گزارش خطا متصلید که خطاهای برنامه شما در گوشی کاربر به شما گزارش داده خواهد شد. این گزارش‌ها شامل:
  • وضعیت گوشی در حین باز شدن برنامه و در حین خطا چگونه بوده است.
  • مشخصات گوشی
  • این خطا به چه تعداد رخ داده است و برای چه تعداد کاربر
  • گزارش گیری بر اساس اولین تاریخ رخداد خطا و آخرین تاریخ، نسخه سیستم عامل اندروید، ورژن برنامه شما و...
و امکانات دیگر.

نه. آکرا همانند Elmah نمی‌تواند خطاهای catch شده را دریافت کند. برای حل این مشکل عبارت زیر را در catch‌ها بنویسید:
ACRA.getErrorReporter().handleException(caughtException);

ده. بر خلاف سیستم دات نت که شما اجباری به استفاده از Try Catch‌ها ندارید. در جاوا اینگونه نیست و هر متدی که Throw روی آن انجام شده باشد مستلزم  استفاده از catch است. به همین دلیل در شماره نه گفتیم که چگونه باید این مشکل را حل کنیم. ولی در بسیاری از اوقات پیش می‌آید که ما داریم از ماژول‌های متفاوتی استفاده میکنیم که جدا از ماژول اصلی برنامه هستند و این مورد باعث می‌شود که بعضی افراد یا Acra را در همه ماژول‌ها صدا بزنند یا اینکه بی خیال آن شوند. ولی کار راحت‌تر این است که شما هم همانند برنامه نویسان جاوا متد خود را به Throw مزین کنید تا در هنگام استفاده از آن در برنامه اصلی نیاز به catch شدن باشد. در واقع شما نباید catch‌ها را داخل یک کتابخانه جدا و مستقل قرار دهید و روش صحیح هم همین است حالا چه استفاده از آکرا نیاز باشد و چه نباشد.

نمونه اشتباه:
 public  void CopyFile(String source,String destination,CopyFileListener copyFileListener) {
        try {
        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(destination);

        long fileLength=new File(source).length();
        // Transfer bytes from in to out
        byte[] buf = new byte[64*1024];
        int len;
        long total=0;
        while ((len = in.read(buf)) > 0) {
            out.write(buf, 0, len);
            total+=len;
            copyFileListener.PublishProgress(fileLength,total);
        }
        in.close();

            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
نمونه صحیح:
    public  void CopyFile(String source,String destination,CopyFileListener copyFileListener) throws IOException {

        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(destination);

        long fileLength=new File(source).length();
        // Transfer bytes from in to out
        byte[] buf = new byte[64*1024];
        int len;
        long total=0;
        while ((len = in.read(buf)) > 0) {
            out.write(buf, 0, len);
            total+=len;
            copyFileListener.PublishProgress(fileLength,total);
        }
        in.close();

            out.close();
    }
  ادامه دارد...
مطالب
معرفی پروژه فروشگاهی Iris Store
پروژه IrisStore، یک سیستم فروشگاهی متن باز برای راه اندازی فروشگاه‌های اینترنتی کوچک است که سورس آن را می‌توانید از آدرس زیر دریافت کنید و برای اجرای آن نیاز به VS 2015 دارید (به دلیل استفاده‌ی از قابلیت‌های جدید زبان سی‌شارپ):
 
https://github.com/MehdiSaeedifar/IrisStore
 
همچنین نمونه‌ی آنلاین آن‌را می‌توانید در فروشگاه آیریس مشاهده کنید.
 

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

جست و جو با قابلیت دسته بندی نتایج

 
به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده می‌شود.



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



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

   
ویرایش اطلاعات به صورت inline
 
امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.



   

مدیریت تصاویر کالا

 
در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آن‌ها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکس‌ها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته می‌شود.)


   

قابلیت‌های دیگر:

 
- مدیریت تصاویر اسلایدشو و تغییر ترتیب آن‌ها از طریق کشیدن و رها کردن (drag & drop)
- تعریف برگه و تغییر ترتیب نمایش آن‌ها از طریق کشیدن و رها کردن
- امکان ارسال پست
- تعریف دسته بندی
- مدیریت کاربران
- تعریف تنظیمات سایت
- نمایش کالا و پست‌های مشابه

کارهایی که باید انجام شود:

 
- پیاده سازی سبد خرید و خرید آنلاین
 

تصویر پنل مدیریت

 

تصویر صفحه‌ی اصلی:



همچنین به راحتی می‌توان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایت‌های زیر نمونه‌های آنلاین دیگری از این سیستم هستند:

- http://www.petrapars.ir
- http://www.ava-tarh.ir
  
در نهایت فهرستی از کتاب خانه‌ها و فناوری‌های استفاده شده و همچنین مقالات مرتبط با این پروژه را قرار داده‌ام.

کتابخانه‌ها و فریم ورک‌های سمت سرور:

 فناوری یا کتابخانه   توضیحات  
مقالات مرتبط
 ASP.NET MVC 5.x
 فریم ورک و موتور اصلی سایت
-ASP.NET MVC
-How to handle repeating form fields in ASP MVC
-How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET  
 Entity Framework 6.x
 فریم ورک دسترسی به داده
-Entity framework code-first
-Update One-to-Many Entity using DBContext 
-مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای
EFSecondLevelCache
کش سطح دوم EF 6
 -بازنویسی سطح دوم کش برای Entity framework 6  
 AutoMapper
 نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار  - دوره AutoMapper
- خودکارسازی فرآیند نگاشت اشیاء در AutoMapper  
 StructureMap
 تزریق وابستگی‌ها
-EF Code First #12  
 MvcCheckBoxList
 اضافه کردن CheckBoxList  به HtmlHelper 

 DNTScheduler
 برای انجام کارهای زمان بندی شده
-انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
 Lucene.Net
 موتور جستجوی سایت  -جستجوی سریع و پیشرفته با لوسین Lucene.net  
 AspNet.Identity
 سیستم مدیریت کاربران
-اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity  
 ELMAH.MVC
 کتابخانه ثبت وقایع و خطا‌های سیستم  -معرفی ELMAH
 PagedList
 نمایش اطلاعات به صورت صفحه بندی شده

PersianDateTime
جایگزینی است برای System.DateTime برای تاریخ‌های شمسی
-PersianDateTime جایگزینی برای System.DateTime
T4MVC
تعاریف Strongly typed مسیرها 
-T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
Dynamic LINQ
نوشتن کوئری‌های LINQ به صورت رشته ای
-انتخاب پویای فیلد‌ها در LINQ
-فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC 

کتابخانه‌های جاوا اسکریپتی سمت کلاینت:
 
 فناوری یا کتابخانه  
  توضیحات     مقالات مرتبط 
 jQuery  کتاب خانه‌ی پایه جاوا اسکرپتی سایت
 -آموزش (jQuery) جی کوئری
-آموزش JQuery Plugin و مباحث پیشرفته جی کوئری  
 
 jQuery UI  ویجت‌های رابط کاربری
- نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC
- jQuery UI Sortable
-Categorized search result with jQuery UI Autocomplete
- jQuery UI Slider
-rtl jQuery UI Slider
-jquery UI Sortable with table and tr width  
jQuery Validation اعتبار سنجی سمت کلاینت
-مشکل اعتبار سنجی jQuery validator در Bootstrap tabs
-نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
toastr نمایش پیام و اطلاع رسانی

PersianDatePicker یک DatePicker شمسی کم حجم 
-PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
CKEDITOR ادیتور متن
-استفاده از ادیتور CKEditor در صفحات ASP.NET
-یکپارچه سازی CKEditor با Lightbox
Roxy Fileman مدیریت فایل ها  -افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  
Magnific Popup نمایش عکس‌ها به صورت پاپ آپ

Select2 تغییر شکل drop down list‌ها برای انتخاب گزینه‌ها

jqGrid v4.6 نمایش اطلاعات در قالب جدول
- آموزش jqGrid
Bootstrap Star Rating امتیاز دهی ستاره ای
-پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
jQuery File Upload Plugin آپلود فایل به صورت AJAX ای

HIGHCHARTS نمایش نمودار

jQuery Number Plugin برای فرمت کردن اعداد

X-editable ویرایش اطلاعات به صورت inline
-قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت اول  
bootstrap-confirmation نمایش فرم تایید در قالب popover

PathJS برای تغییر URL صفحه برای اعمال Ajax ای 
-پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC  

فریمورک‌های CSS:
 
فناوری یا کتابخانه
 توضیحات  
 مقالات مرتبط  
 Bootstrap 3.x 
 فریم ورک پایه ای css سایت
 - Bootstrap 3 RTL Theme
- Twitter Bootstrap
-سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC 
-ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC
-نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
 AdminLTE 
 قالب مدیریت سایت
 - نسخه راستچین شده AdminLTE 2.2.1
Animate.css   انیمیشن‌های css3 سایت

Font Awesome   پک آیکون‌های برداری

Awesome Bootstrap Checkbox   زیبا سازی چک باکس ها

فونت فارسی وزیر   قلم فارسی



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

https://github.com/MehdiSaeedifar/IrisStore 

همچنین نمونه‌ی آنلاین آن‌را می‌توانید در فروشگاه آیریس مشاهده کنید.


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

جست و جو با قابلیت دسته بندی نتایج
 

به هنگام جست و جو، لیستی از موارد پیشنهادی به صورت دسته بندی شده نمایش داده می‌شود. 



جست و جوی پیشرفته کالا‌ها 

جست و جو بر اساس قیمت، گروه، کلمات کلیدی و مرتب سازی نتایج انجام می‌گیرد. همچنین نتایج جست و جو بدون رفرش شدن صفحه و به صورت AJAX ای به همراه تغییر URL صفحه صورت می‌گیرد. 



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


ویرایش اطلاعات به صورت inline 
 
امکان ویرایش قیمت و تاریخ به صورت inline وجود دارد.



 

مدیریت تصاویر کالا

  
در این قسمت امکان آپلود همزمان چندین فایل به همراه پیش نمایش آن‌ها وجود دارد. همچنین امکان کشیدن و رها کردن برای تغییر ترتیب چیدمان عکس‌ها نیز مهیا است.( تصویر اول به عنوان کاور کالا در نظر گرفته می‌شود.)


 

قابلیت‌های دیگر:

  
- مدیریت تصاویر اسلایدشو و تغییر ترتیب آن‌ها از طریق کشیدن و رها کردن (drag & drop)
- تعریف برگه و تغییر ترتیب نمایش آن‌ها از طریق کشیدن و رها کردن
- امکان ارسال پست
- تعریف دسته بندی
- مدیریت کاربران
- تعریف تنظیمات سایت
- نمایش کالا و پست‌های مشابه


تصویر پنل مدیریت


تصویر صفحه‌ی اصلی:



همچنین به راحتی می‌توان با طراحی قالب جدیدی، از این سیستم برای کاری غیر از فروشگاه اینترنتی استفاده کرد؛ سایت‌های زیر نمونه‌های آنلاین دیگری از این سیستم هستند:

http://www.petrapars.ir 
http://www.ava-tarh.ir 

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

کتابخانه‌ها و فریم ورک‌های سمت سرور:

 فناوری یا کتابخانه   توضیحات
مقالات مرتبط
 ASP.NET MVC 5.x 
 فریم ورک و موتور اصلی سایت
-ASP.NET MVC 
-How to handle repeating form fields in ASP MVC 
-How to dynamically (via AJAX) add new items to a bound list model, in ASP MVC.NET  
 Entity Framework 6.x 
 فریم ورک دسترسی به داده
-Entity framework code-first 
-Update One-to-Many Entity using DBContext
-مدیریت اطلاعات وابسته به زمان در بانک‌های اطلاعاتی رابطه‌ای 
EFSecondLevelCache 
کش سطح دوم EF 6
 -بازنویسی سطح دوم کش برای Entity framework 6
 AutoMapper 
 نگاشت اطلاعات یک شی به شی دیگر به صورت خودکار  دوره AutoMapper 
خودکارسازی فرآیند نگاشت اشیاء در AutoMapper
 StructureMap 
 تزریق وابستگی‌ها 
-EF Code First #12
 MvcCheckBoxList 
 اضافه کردن CheckBoxList  به HtmlHelper

 DNTScheduler 
 برای انجام کارهای زمان بندی شده
-انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
 Lucene.Net 
 موتور جستجوی سایت  -جستجوی سریع و پیشرفته با لوسین Lucene.net
 AspNet.Identity 
 سیستم مدیریت کاربران
-اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
 ELMAH.MVC 
 کتابخانه ثبت وقایع و خطا‌های سیستم  -معرفی ELMAH
 PagedList 
 نمایش اطلاعات به صورت صفحه بندی شده

PersianDateTime 
جایگزینی است برای System.DateTime برای تاریخ‌های شمسی 
-PersianDateTime جایگزینی برای System.DateTime
T4MVC 
تعاریف Strongly typed مسیرها
-T4MVC : یکی از الزامات مدیریت پروژه‌های ASP.NET MVC
Dynamic LINQ 
نوشتن کوئری‌های LINQ به صورت رشته ای
-انتخاب پویای فیلد‌ها در LINQ 
-فعال سازی و پردازش جستجوی پویای jqGrid در ASP.NET MVC

کتابخانه‌های جاوا اسکریپتی سمت کلاینت:

 فناوری یا کتابخانه
  توضیحات     مقالات مرتبط
 jQuery  کتاب خانه‌ی پایه جاوا اسکرپتی سایت
 -آموزش (jQuery) جی کوئری 
-آموزش JQuery Plugin و مباحث پیشرفته جی کوئری

 jQuery UI  ویجت‌های رابط کاربری
نمایش رکوردها به ترتیب اولویت به کمک jQuery UI sortable در ASP.NET MVC 
jQuery UI Sortable 
-Categorized search result with jQuery UI Autocomplete 
jQuery UI Slider 
-rtl jQuery UI Slider 
-jquery UI Sortable with table and tr width
jQuery Validation اعتبار سنجی سمت کلاینت
-مشکل اعتبار سنجی jQuery validator در Bootstrap tabs 
-نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
toastr نمایش پیام و اطلاع رسانی

PersianDatePicker یک DatePicker شمسی کم حجم
-PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند
CKEDITOR ادیتور متن
-استفاده از ادیتور CKEditor در صفحات ASP.NET 
-یکپارچه سازی CKEditor با Lightbox 
Roxy Fileman مدیریت فایل ها  -افزونه مدیریت فایل‌های رایگان Roxy FileMan برای TinyMce و CkEditor  
Magnific Popup نمایش عکس‌ها به صورت پاپ آپ

Select2 تغییر شکل drop down list‌ها برای انتخاب گزینه‌ها

jqGrid v4.6 نمایش اطلاعات در قالب جدول
آموزش jqGrid
Bootstrap Star Rating امتیاز دهی ستاره ای
-پیاده سازی امتیاز دهی ستاره‌ای به مطالب به کمک jQuery در ASP.NET MVC
jQuery File Upload Plugin آپلود فایل به صورت AJAX ای

HIGHCHARTS نمایش نمودار

jQuery Number Plugin برای فرمت کردن اعداد

X-editable ویرایش اطلاعات به صورت inline
-قابل ویرایش کننده‌ی فوق العاده x-editable ؛ قسمت اول
bootstrap-confirmation نمایش فرم تایید در قالب popover

PathJS برای تغییر URL صفحه برای اعمال Ajax ای
-پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC

فریمورک‌های CSS:

فناوری یا کتابخانه 
 توضیحات
 مقالات مرتبط
 Bootstrap 3.x
 فریم ورک پایه ای css سایت
 - Bootstrap 3 RTL Theme 
Twitter Bootstrap 
-سازگارسازی کلاس‌های اعتبارسنجی Twitter Bootstrap 3 با فرم‌های ASP.NET MVC
-ساخت قالب‌های نمایشی و ادیتور دکمه سه وضعیتی سازگار با Twitter bootstrap در ASP.NET MVC 
-نمایش اخطارها و پیام‌های بوت استرپ به کمک TempData در ASP.NET MVC
 AdminLTE
 قالب مدیریت سایت
 - نسخه راستچین شده AdminLTE 2.2.1
Animate.css انیمیشن‌های css3 سایت

Font Awesome پک آیکون‌های برداری

Awesome Bootstrap Checkbox زیبا سازی چک باکس ها

فونت فارسی وزیر قلم فارسی
 

نظرات مطالب
بهبود SEO در ASP.NET MVC
- تشخیص وجود کوئری استرینگ در یک Url، حالت دیگری نمی‌تونه داشته باشه.
- ضمنا یک Url می‌تونه کوئری استرینگی با چندین name-value داشته باشه. ضرورتی به بررسی تمام موارد و کند کردن کار نیست که آیا مقدار دارند همگی یا خیر.
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت ششم - تکمیل مستندات محافظت از API
ممکن است تعدادی از اکشن متدهای API طراحی شده، محافظت شده باشند. بنابراین OpenAPI Specification تولیدی نیز باید به همراه مستندات کافی در این مورد باشد تا استفاده کنندگان از آن بدانند چگونه باید با آن کار کنند. نگارش سوم OpenAPI Specification از اعتبارسنجی و احراز هویت مبتنی بر هدرها مانند basic و یا bearer، همچنین حالت کار با API Keys مانند هدرها، کوئری استرینگ‌ها و کوکی‌ها و یا حالت OAuth2 و OpenID Connect پشتیبانی می‌کند و این موارد ذیل خواص securitySchemes و security در OpenAPI Specification ظاهر می‌شوند:
"securitySchemes":
{ "basicAuth":
       {
             "type":"http",
             "description":"Input your username and password to access this API",
             "scheme":"basic"
       }
}
…
"security":[
  {"basicAuth":[]}
]
- خاصیت securitySchemes انواع حالت‌های اعتبارسنجی پشتیبانی شده را لیست می‌کند.
- خاصیت security کار اعمال Scheme تعریف شده را به کل API یا صرفا قسمت‌های خاصی از آن، انجام می‌دهد.

در ادامه مثالی را بررسی خواهیم کرد که مبتنی بر basic authentication کار می‌کند و در این حالت به ازای هر درخواست به API، نیاز است یک نام کاربری و کلمه‌ی عبور نیز ارسال شوند. البته روش توصیه شده، کار با JWT و یا OpenID Connect است؛ اما جهت تکمیل ساده‌تر این قسمت، بدون نیاز به برپایی مقدماتی پیچیده، کار با basic authentication را بررسی می‌کنیم و اصول کلی آن از دیدگاه مستندات OpenAPI Specification تفاوتی نمی‌کند.


افزودن Basic Authentication به API برنامه

برای پیاده سازی Basic Authentication نیاز به یک AuthenticationHandler سفارشی داریم:
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;

namespace OpenAPISwaggerDoc.Web.Authentication
{
    public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public BasicAuthenticationHandler(
            IOptionsMonitor<AuthenticationSchemeOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder,
            ISystemClock clock)
            : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            if (!Request.Headers.ContainsKey("Authorization"))
            {
                return Task.FromResult(AuthenticateResult.Fail("Missing Authorization header"));
            }

            try
            {
                var authenticationHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
                var credentialBytes = Convert.FromBase64String(authenticationHeader.Parameter);
                var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
                var username = credentials[0];
                var password = credentials[1];

                if (username == "DNT" && password == "123")
                {
                    var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
                    var identity = new ClaimsIdentity(claims, Scheme.Name);
                    var principal = new ClaimsPrincipal(identity);
                    var ticket = new AuthenticationTicket(principal, Scheme.Name);
                    return Task.FromResult(AuthenticateResult.Success(ticket));
                }
                return Task.FromResult(AuthenticateResult.Fail("Invalid username or password"));
            }
            catch
            {
                return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization header"));
            }
        }
    }
}
کار این AuthenticationHandler سفارشی، با بازنویسی متد HandleAuthenticateAsync شروع می‌شود. در اینجا به دنبال هدر ویژه‌ای با کلید Authorization می‌گردد. این هدر باید به همراه نام کاربری و کلمه‌ی عبوری با حالت base64 encoded باشد. اگر این هدر وجود نداشت و یا مقدار هدر Authorization، با فرمتی که مدنظر ما است قابل decode و همچنین جداسازی نبود، شکست اعتبارسنجی اعلام می‌شود.
پس از دریافت مقدار هدر Authorization، ابتدا مقدار آن‌را از base64 به حالت معمولی تبدیل کرده و سپس بر اساس حرف ":"، دو قسمت را از آن جداسازی می‌کنیم. قسمت اول را به عنوان نام کاربری و قسمت دوم را به عنوان کلمه‌ی عبور پردازش خواهیم کرد. در این مثال جهت سادگی، این دو باید مساوی DNT و 123 باشند. اگر اینچنین بود، یک AuthenticationTicket دارای Claim ای حاوی نام کاربری را ایجاد کرده و آن‌را به عنوان حاصل موفقیت آمیز بودن عملیات بازگشت می‌دهیم.

مرحله‌ی بعد، استفاده و معرفی این BasicAuthenticationHandler تهیه شده به برنامه است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(defaultScheme: "Basic")
                    .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);
در اینجا توسط متد services.AddAuthentication، این scheme جدید که نام رسمی آن Basic است، به همراه Handler آن، به برنامه معرفی می‌شود.
همچنین نیاز است میان‌افزار اعتبارسنجی را نیز با فراخوانی متد app.UseAuthentication، به برنامه اضافه کرد که باید پیش از فراخوانی app.UseMvc صورت گیرد تا به آن اعمال شود:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
   // ...
            app.UseStaticFiles();

            app.UseAuthentication();

            app.UseMvc();
        }
    }
}

همچنین برای اینکه تمام اکشن متدهای موجود را نیز محافظت کنیم، می‌توان فیلتر Authorize را به صورت سراسری اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
     // ...
   
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new AuthorizeFilter());
        // ...


تکمیل مستندات API جهت انعکاس تنظیمات محافظت از اکشن متدهای آن

پس از تنظیم محافظت دسترسی به اکشن متدهای برنامه، اکنون نوبت به مستند کردن آن است و همانطور که در ابتدای بحث نیز عنوان شد، برای این منظور نیاز به تعریف خواص securitySchemes و security در OpenAPI Specification است:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
   // ...

                setupAction.AddSecurityDefinition("basicAuth", new OpenApiSecurityScheme
                {
                    Type = SecuritySchemeType.Http,
                    Scheme = "basic",
                    Description = "Input your username and password to access this API"
                });
            });
در اینجا توسط متد setupAction.AddSecurityDefinition، ابتدا یک نام تعریف می‌شود (از این نام، در قسمت بعدی تنظیمات استفاده خواهد شد). پارامتر دوم آن همان SecurityScheme است که توضیح داده شد. برای حالت basic auth، نوع آن Http است و اسکیمای آن basic. باید دقت داشت که مقدار خاصیت Scheme در اینجا، حساس به بزرگی و کوچکی حروف است.

پس از این تنظیم اگر برنامه را اجرا کنیم، یک دکمه‌ی authorize اضافه شده‌است:


با کلیک بر روی آن، صفحه‌ی ورود نام کاربری و کلمه‌ی عبور ظاهر می‌شود:


اگر آن‌را تکمیل کرده و سپس برای مثال لیست نویسندگان را درخواست کنیم (با کلیک بر روی دکمه‌ی try it out آن و سپس کلیک بر روی دکمه‌ی execute ذیل آن)، تنها خروجی 401 یا unauthorized را دریافت می‌کنیم:


- بنابراین برای تکمیل آن، مطابق نکات قسمت چهارم، ابتدا باید status code مساوی 401 را به صورت سراسری، مستند کنیم:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(setupAction =>
            {
                setupAction.Filters.Add(new ProducesResponseTypeAttribute(StatusCodes.Status401Unauthorized));

- همچنین هرچند با کلیک بر روی دکمه‌ی Authorize در Swagger UI و ورود نام کاربری و کلمه‌ی عبور توسط آن، در همانجا پیام Authorized را دریافت کردیم، اما اطلاعات آن به ازای هر درخواست، به سمت سرور ارسال نمی‌شود. به همین جهت در حین درخواست لیست نویسندگان، پیام unauthorized را دریافت کردیم. برای رفع این مشکل نیاز است به OpenAPI Spec اعلام کنیم که تعامل با API، نیاز به Authentication دارد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(setupAction =>
            {
    // ...

                setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.SecurityScheme,
                                Id = "basicAuth"
                            }
                        },
                        new List<string>()
                    }
                });
            });
در اینجا OpenApiSecurityRequirement یک دیکشنری از نوع <<Dictionary<OpenApiSecurityScheme, IList<string است که کلید آن از نوع OpenApiSecurityScheme تعریف می‌شود و باید آن‌را به نمونه‌ای که توسط setupAction.AddSecurityDefinition پیشتر اضافه کردیم، متصل کنیم. این اتصال توسط خاصیت Reference آن و Id ای که به نام تعریف شده‌ی توسط آن اشاره می‌کند، صورت می‌گیرد. مقدار این دیکشنری نیز لیستی از رشته‌ها می‌تواند باشد (مانند توکن‌ها و scopes در OpenID Connect) که در اینجا با یک لیست خالی مقدار دهی شده‌است.

پس از این تنظیمات، Swagger UI با افزودن یک آیکن قفل به مداخل APIهای محافظت شده، به صورت زیر تغییر می‌کند:


در این حالت اگر بر روی آیکن قفل کلیک کنیم، همان صفحه‌ی دیالوگ ورود نام کاربری و کلمه‌ی عبوری که پیشتر با کلیک بر روی دکمه‌ی Authorize ظاهر شد، نمایش داده می‌شود. با تکمیل آن و کلیک مجدد بر روی آیکن قفل، جهت گشوده شدن پنل API و سپس کلیک بر روی try it out  آن، برای مثال می‌توان به API محافظت شده‌ی دریافت لیست نویسندگان، بدون مشکلی، دسترسی یافت:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-06.zip
نظرات مطالب
چک لیست تهیه یک برنامه ASP.NET MVC
فرض کنید که در UI فقط به نام کاربر و آدرس ایمیل کاربر احتیاج است،کلاس کاربر در در Domain به شرح زیر است:
public class User
{
   public int UserId {get;set;}
   public string Name {get;set;}
   public string Family {get;set;}
   public string Web {get;set;}
   public string Email {get;set;}
   public DateTime RegisterDate {get;set;}
}
حالا در یک کنترلر فقط به نام کاربر و آدرس ایمیل کاربر احتیاج است،حالا میایم یک کلاس تعریف میکنیم که شامل فیلدهای مورد نیاز است:
public class UserInfoViewModel
{
   public string Name {get;set;}
   public string Family {get;set;}
   public string Email {get;set;}
}
و لایه سرویس یک متد منحصر بفرد مختص این کار می نویسیم ،مثلاً:
public UserInfoViewModel GetMemberByUserName(string username)
{
   var result = from u in _users
                   where u.UserName == username
                   select new UserInfoViewModel() {Name = u.Name,Family=u.Family,Email=u.Email};

   return result;
}
1-اگه این روش درسته،حالا:
  • - برای هر متد که نیازه یه سری فیلد مورد نیاز رو برگردونه،یه کلاس جداگانه باید تعریف کرد؟(در اینجا UserInfoViewModel
  • - این کلاس UserInfoViewModel باید جز ViewModel‌های لایه UI باشه و در لایه Models قرار بگیره؟
      در اینصورت (کلاس UserInfoViewModel باید جز ViewModel‌های لایه UI باشه )،لایه سرویس   وابسته به لایه UI نمیشه؟
      اگر جواب منفیه،
      کلاس UserInfoViewModel تو کدوم لایه باید قرار بگیره؟
      دیگه نباید پسوند ViewModel رو به این کلاس اضافه کرد،درسته؟
2-اگه این روش کلاً اشتباهه،راه حل شما دقیقاً چیه؟
مطالب
پیاده سازی Password Policy سفارشی توسط ASP.NET Identity
برای فراهم کردن یک تجربه کاربری ایمن‌تر و بهتر، ممکن است بخواهید پیچیدگی password policy را سفارشی سازی کنید. مثلا ممکن است بخواهید حداقل تعداد کاراکتر‌ها را تنظیم کنید، استفاده از چند حروف ویژه را اجباری کنید،  جلوگیری از استفاده نام کاربر در کلمه عبور و غیره. برای اطلاعات بیشتر درباره سیاست‌های کلمه عبور به این لینک مراجعه کنید. بصورت پیش فرض ASP.NET Identity کاربران را وادار می‌کند تا کلمه‌های عبوری بطول حداقل 6 کاراکتر وارد نمایند. در ادامه نحوه افزودن چند خط مشی دیگر را هم بررسی می‌کنیم.

با استفاده از ویژوال استودیو 2013 پروژه جدیدی خواهیم ساخت تا از ASP.NET Identity استفاده کند. مواردی که درباره کلمه‌های عبور می‌خواهیم اعمال کنیم در زیر لیست شده اند.

  • تنظیمات پیش فرض باید تغییر کنند تا کلمات عبور حداقل 10 کاراکتر باشند
  • کلمه عبور حداقل یک عدد و یک کاراکتر ویژه باید داشته باشد
  • امکان استفاده از 5 کلمه عبور اخیری که ثبت شده وجود ندارد
در آخر اپلیکیشن را اجرا می‌کنیم و عملکرد این قوانین جدید را بررسی خواهیم کرد.


ایجاد اپلیکیشن جدید

در Visual Studio 2013 اپلیکیشن جدیدی از نوع ASP.NET MVC 4.5 بسازید.

در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Manage NuGet Packages را انتخاب کنید. به قسمت Update بروید و تمام انتشارات جدید را در صورت وجود نصب کنید.

بگذارید تا به روند کلی ایجاد کاربران جدید در اپلیکیشن نگاهی بیاندازیم. این به ما در شناسایی نیازهای جدیدمان کمک می‌کند. در پوشه Controllers فایلی بنام AccountController.cs وجود دارد که حاوی متدهایی برای مدیریت کاربران است.

  • کنترلر Account از کلاس UserManager استفاده می‌کند که در فریم ورک Identity تعریف شده است. این کلاس به نوبه خود از کلاس دیگری بنام UserStore استفاده می‌کند که برای دسترسی و مدیریت داده‌های کاربران استفاده می‌شود. در مثال ما این کلاس از Entity Framework استفاده می‌کند که پیاده سازی پیش فرض است.
  • متد Register POST یک کاربر جدید می‌سازد. متد CreateAsync به طبع متد 'ValidateAsync' را روی خاصیت PasswordValidator فراخوانی می‌کند تا کلمه عبور دریافتی اعتبارسنجی شود.
var user = new ApplicationUser() { UserName = model.UserName };  
var result = await UserManager.CreateAsync(user, model.Password);
  
if (result.Succeeded)
{
    await SignInAsync(user, isPersistent: false);  
    return RedirectToAction("Index", "Home");
}

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


قانون 1: کلمه‌های عبور باید حداقل 10 کاراکتر باشند

بصورت پیش فرض خاصیت PasswordValidator در کلاس UserManager به کلاس MinimumLengthValidator تنظیم شده است، که اطمینان حاصل می‌کند کلمه عبور حداقل 6 کاراکتر باشد. هنگام وهله سازی UserManager می‌توانید این مقدار را تغییر دهید.
  • مقدار حداقل کاراکترهای کلمه عبور به دو شکل می‌تواند تعریف شود. راه اول، تغییر کنترلر Account است. در متد سازنده این کنترلر کلاس UserManager وهله سازی می‌شود، همینجا می‌توانید این تغییر را اعمال کنید. راه دوم، ساختن کلاس جدیدی است که از UserManager ارث بری می‌کند. سپس می‌توان این کلاس را در سطح global تعریف کرد. در پوشه IdentityExtensions کلاس جدیدی با نام ApplicationUserManager بسازید.
public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(): base(new UserStore<ApplicationUser>(new ApplicationDbContext()))
    {
        PasswordValidator = new MinimumLengthValidator (10);
    }
}

  کلاس UserManager یک نمونه از کلاس IUserStore را دریافت می‌کند که پیاده سازی API‌های مدیریت کاربران است. از آنجا که کلاس UserStore مبتنی بر Entity Framework است، باید آبجکت DbContext را هم پاس دهیم. این کد در واقع همان کدی است که در متد سازنده کنترلر Account وجود دارد.

یک مزیت دیگر این روش این است که می‌توانیم متدهای UserManager را بازنویسی (overwrite) کنیم. برای پیاده سازی نیازمندهای بعدی دقیقا همین کار را خواهیم کرد.


  • حال باید کلاس ApplicationUserManager را در کنترلر Account استفاده کنیم. متد سازنده و خاصیت UserManager را مانند زیر تغییر دهید.
 public AccountController() : this(new ApplicationUserManager())
         {
         }
  
         public AccountController(ApplicationUserManager userManager)
         {
             UserManager = userManager;
         }
         public ApplicationUserManager UserManager { get; private set; }
حالا داریم از کلاس سفارشی جدیدمان استفاده می‌کنیم. این به ما اجازه می‌دهد مراحل بعدی سفارشی سازی را انجام دهیم، بدون آنکه کدهای موجود در کنترلر از کار بیافتند.
  • اپلیکیشن را اجرا کنید و سعی کنید کاربر محلی جدیدی ثبت نمایید. اگر کلمه عبور وارد شده کمتر از 10 کاراکتر باشد پیغام خطای زیر را دریافت می‌کنید.


قانون 2: کلمه‌های عبور باید حداقل یک عدد و یک کاراکتر ویژه داشته باشند

چیزی که در این مرحله نیاز داریم کلاس جدیدی است که اینترفیس IIdentityValidator را پیاده سازی می‌کند. چیزی که ما می‌خواهیم اعتبارسنجی کنیم، وجود اعداد و کاراکترهای ویژه در کلمه عبور است، همچنین طول مجاز هم بررسی می‌شود. نهایتا این قوانین اعتبارسنجی در متد 'ValidateAsync' بکار گرفته خواهند شد.
  • در پوشه IdentityExtensions کلاس جدیدی بنام CustomPasswordValidator بسازید و اینترفیس مذکور را پیاده سازی کنید. از آنجا که نوع کلمه عبور رشته (string) است از <IIdentityValidator<string استفاده می‌کنیم.
public class CustomPasswordValidator : IIdentityValidator<string> 
{  
    public int RequiredLength { get; set; }

    public CustomPasswordValidator(int length)
    {
        RequiredLength = length;
    }
  
    public Task<IdentityResult> ValidateAsync(string item)
    {
        if (String.IsNullOrEmpty(item) || item.Length < RequiredLength)
        {
            return Task.FromResult(IdentityResult.Failed(String.Format("Password should be of length {0}",RequiredLength)));
        }
  
    string pattern = @"^(?=.*[0-9])(?=.*[!@#$%^&*])[0-9a-zA-Z!@#$%^&*0-9]{10,}$";
   
    if (!Regex.IsMatch(item, pattern))  
    {
        return Task.FromResult(IdentityResult.Failed("Password should have one numeral and one special character"));
    }
  
    return Task.FromResult(IdentityResult.Success);
 }


  در متد ValidateAsync بررس می‌کنیم که طول کلمه عبور معتبر و مجاز است یا خیر. سپس با استفاده از یک RegEx وجود کاراکترهای ویژه و اعداد را بررسی می‌کنیم. دقت کنید که regex استفاده شده تست نشده و تنها بعنوان یک مثال باید در نظر گرفته شود.
  • قدم بعدی تعریف این اعتبارسنج سفارشی در کلاس UserManager است. باید مقدار خاصیت PasswordValidator را به این کلاس تنظیم کنیم. به کلاس ApplicationUserManager که پیشتر ساختید بروید و مقدار خاصیت PasswordValidator را به CustomPasswordValidator تغییر دهید.
public class ApplicationUserManager : UserManager<ApplicationUser>  
{  
    public ApplicationUserManager() : base(new UserStore<ApplicationUser(new ApplicationDbContext()))
    {
        PasswordValidator = new CustomPasswordValidator(10);
    }
 }

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


قانون 3: امکان استفاده از 5 کلمه عبور اخیر ثبت شده وجود ندارد

هنگامی که کاربران سیستم، کلمه عبور خود را بازنشانی (reset) می‌کنند یا تغییر می‌دهند، می‌توانیم بررسی کنیم که آیا مجددا از یک کلمه عبور پیشین استفاده کرده اند یا خیر. این بررسی بصورت پیش فرض انجام نمی‌شود، چرا که سیستم Identity تاریخچه کلمه‌های عبور کاربران را ذخیره نمی‌کند. می‌توانیم در اپلیکیشن خود جدول جدیدی بسازیم و تاریخچه کلمات عبور کاربران را در آن ذخیره کنیم. هربار که کاربر سعی در بازنشانی یا تغییر کلمه عبور خود دارد، مقدار Hash شده را در جدول تاریخچه بررسی میکنیم.
فایل IdentityModels.cs را باز کنید. مانند لیست زیر، کلاس جدیدی بنام 'PreviousPassword' بسازید.
public class PreviousPassword  
{
  
 public PreviousPassword()
 {
    CreateDate = DateTimeOffset.Now;
 }
  
 [Key, Column(Order = 0)]
  
 public string PasswordHash { get; set; }
  
 public DateTimeOffset CreateDate { get; set; }
  
 [Key, Column(Order = 1)]
  
 public string UserId { get; set; }
  
 public virtual ApplicationUser User { get; set; }
  
 }
در این کلاس، فیلد 'Password' مقدار Hash شده کلمه عبور را نگاه میدارد و توسط فیلد 'UserId' رفرنس می‌شود. فیلد 'CreateDate' یک مقدار timestamp ذخیره می‌کند که تاریخ ثبت کلمه عبور را مشخص می‌نماید. توسط این فیلد می‌توانیم تاریخچه کلمات عبور را فیلتر کنیم و مثلا 5 رکورد آخر را بگیریم.

Entity Framework Code First جدول 'PreviousPasswords' را می‌سازد و با استفاده از فیلدهای 'UserId' و 'Password' کلید اصلی (composite primary key) را ایجاد می‌کند. برای اطلاعات بیشتر درباره قرارداهای EF Code First به  این لینک  مراجعه کنید.

  • خاصیت جدیدی به کلاس ApplicationUser اضافه کنید تا لیست آخرین کلمات عبور استفاده شده را نگهداری کند.
public class ApplicationUser : IdentityUser
{
    public ApplicationUser() : base()
    {
        PreviousUserPasswords = new List<PreviousPassword>();
     }

     public virtual IList<PreviousPassword> PreviousUserPasswords { get; set; }
}

 همانطور که پیشتر گفته شد، کلاس UserStore پیاده سازی API‌های لازم برای مدیریت کاربران را در بر می‌گیرد. هنگامی که کاربر برای نخستین بار در سایت ثبت می‌شود باید مقدار Hash کلمه عبورش را در جدول تاریخچه کلمات عبور ذخیره کنیم. از آنجا که UserStore بصورت پیش فرض متدی برای چنین عملیاتی معرفی نمی‌کند، باید یک override تعریف کنیم تا این مراحل را انجام دهیم. پس ابتدا باید کلاس سفارشی جدیدی بسازیم که از UserStore ارث بری کرده و آن را توسعه می‌دهد. سپس از این کلاس سفارشی در ApplicationUserManager بعنوان پیاده سازی پیش فرض UserStore استفاده می‌کنیم. پس کلاس جدیدی در پوشه IdentityExtensions ایجاد کنید.
public class ApplicationUserStore : UserStore<ApplicationUser>
{ 
    public ApplicationUserStore(DbContext context) : base(context)  { }
  
    public override async Task CreateAsync(ApplicationUser user)
    {
        await base.CreateAsync(user);
        await AddToPreviousPasswordsAsync(user, user.PasswordHash);
    }
  
     public Task AddToPreviousPasswordsAsync(ApplicationUser user, string password)
     {
        user.PreviousUserPasswords.Add(new PreviousPassword() { UserId = user.Id, PasswordHash = password });
  
        return UpdateAsync(user);
     }
 }

 متد 'AddToPreviousPasswordsAsync' کلمه عبور را در جدول 'PreviousPasswords' ذخیره می‌کند.

هرگاه کاربر سعی در بازنشانی یا تغییر کلمه عبورش دارد باید این متد را فراخوانی کنیم. API‌های لازم برای این کار در کلاس UserManager تعریف شده اند. باید این متدها را override کنیم و فراخوانی متد مذکور را پیاده کنیم. برای این کار کلاس ApplicationUserManager را باز کنید و متدهای ChangePassword و ResetPassword را بازنویسی کنید.
public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        private const int PASSWORD_HISTORY_LIMIT = 5;

        public ApplicationUserManager() : base(new ApplicationUserStore(new ApplicationDbContext()))
        {
            PasswordValidator = new CustomPasswordValidator(10);
        }

        public override async Task<IdentityResult> ChangePasswordAsync(string userId, string currentPassword, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ChangePasswordAsync(userId, currentPassword, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        public override async Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword)
        {
            if (await IsPreviousPassword(userId, newPassword))
            {
                return await Task.FromResult(IdentityResult.Failed("Cannot reuse old password"));
            }

            var result = await base.ResetPasswordAsync(userId, token, newPassword);

            if (result.Succeeded)
            {
                var store = Store as ApplicationUserStore;
                await store.AddToPreviousPasswordsAsync(await FindByIdAsync(userId), PasswordHasher.HashPassword(newPassword));
            }

            return result;
        }

        private async Task<bool> IsPreviousPassword(string userId, string newPassword)
        {
            var user = await FindByIdAsync(userId);

            if (user.PreviousUserPasswords.OrderByDescending(x => x.CreateDate).
                Select(x => x.PasswordHash).Take(PASSWORD_HISTORY_LIMIT)
                .Where(x => PasswordHasher.VerifyHashedPassword(x, newPassword) != PasswordVerificationResult.Failed).Any())
            {
                return true;
            }

            return false;
        }
    }
فیلد 'PASSWORD_HISTORY_LIMIT' برای دریافت X رکورد از جدول تاریخچه کلمه عبور استفاده می‌شود. همانطور که می‌بینید از متد سازنده کلاس ApplicationUserStore برای گرفتن متد جدیدمان استفاده کرده ایم. هرگاه کاربری سعی می‌کند کلمه عبورش را بازنشانی کند یا تغییر دهد، کلمه عبورش را با 5 کلمه عبور قبلی استفاده شده مقایسه می‌کنیم و بر این اساس مقدار true/false بر می‌گردانیم.

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

سورس کد این مثال را می‌توانید از این لینک دریافت کنید. نام پروژه Identity-PasswordPolicy است، و زیر قسمت Samples/Identity قرار دارد.