مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت دهم - MobX Hooks و اعمال Async در Mobx
روشی را که تا اینجا در مورد MobX بررسی کردیم، تا نگارش 5x آن‌را پوشش می‌دهد. در همین زمان، کتابخانه‌ی دیگری به نام mobx-react-lite ارائه شد که به همراه تعدادی Hook مخصوص MobX بود تا با سیستم جدید React که مبتنی بر Hooks است، سازگار شود. این امکانات در حال حاضر با خود کتابخانه‌ی mobx-react 6x یکپارچه شده و به زودی mobx-react-lite منسوخ شده اعلام می‌شود. البته روش inject/observer بررسی شده‌ی تا نگارش 5x آن، هنوز هم برقرار است و قرار نیست که به این زودی‌ها منسوخ شده اعلام شود. به همین جهت نکاتی را که در مطلب جاری بررسی می‌کنیم، به عنوان روش تکمیلی سازگار با نگارش جاری 6x آن مطرح است و در کل با هر روشی که علاقمند بودید می‌توانید با MobX کار کنید. البته باز هم توصیه شده‌است که سیستم Provider آن‌را با React Context استاندارد، جایگزین کنید؛ چون احتمال حذف آن در نگارش‌های بعدی MobX هست.

به صورت خلاصه:
- اگر فقط از کامپوننت‌های کلاسی استفاده می‌کنید، mobx-react@5 برای کار شما پاسخگو است.
- اگر از کامپوننت‌های کلاسی و همچنین کامپوننت‌های تابعی در برنامه‌ی خود استفاده می‌کنید، mobx-react@6 به همراه mobx-react-lite نیز ارائه می‌شود و هر دو روش را با هم پوشش می‌دهد.
- اگر فقط از کامپوننت‌های تابعی جدید استفاده می‌کنید، هوک‌های کتابخانه‌ی کوچک mobx-react-lite برای کار شما کافی است.


معرفی useLocalStore Hook و useObserver Hook

در مطالب قبلی، روش تعریف یک کلاس مخزن حالت MobX را توسط تزئین کننده‌هایی مانند observable، computed و action بررسی کردیم. همچنین دریافتیم که تعریف یک چنین تزئین کننده‌هایی، یا نیاز به استفاده‌ی از تایپ‌اسکریپت را دارد و یا باید پروژه‌ی React را جهت تغییر کامپایلر Babel آن و فعالسازی decorators، مقداری ویرایش کرد. با useLocalStore Hook هرچند تمام روش‌های قبلی هنوز هم پشتیبانی می‌شوند، اما دیگر نیاز به استفاده‌ی از decorators نیست. useLocalStore تابعی است که یک شیء را باز می‌گرداند. هر خاصیتی از این شیء، به صورت خودکار observable درنظر گرفته می‌شود. تمام getters آن به عنوان computed properties تفسیر می‌شوند و تمام متدهای آن، action درنظر گرفته خواهند شد.
یک مثال:
import React from 'react'
import { useLocalStore, useObserver } from 'mobx-react' // 6.x

export const SmartTodo = () => {
  const todo = useLocalStore(() => ({
    title: 'Click to toggle',
    done: false,
    toggle() {
      todo.done = !todo.done
    },
    get emoji() {
      return todo.done ? '😜' : '🏃'
    },
  }))

  return useObserver(() => (
    <h3 onClick={todo.toggle}>
      {todo.title} {todo.emoji}
    </h3>
  ))
}
- در اینجا نحوه‌ی import تابع useLocalStore را از کتابخانه‌ی mobx-react نگارش 6x ملاحظه می‌کنید.
- روش استفاده‌ی از تابع useLocalStore، می‌تواند به صورت محلی (همانند اسم آن) مختص به یک کامپوننت باشد. یعنی می‌توان بجای state استاندارد React که اجازه‌ی تغییر مستقیم خواص آن‌را نمی‌دهد، از MobX State محلی ارائه شده‌ی توسط useLocalStore استفاده کرد و یا می‌توان useLocalStore را به صورت global نیز تعریف کرد که در ادامه‌ی بحث به آن می‌پردازیم.
- در مثال فوق، طول عمر شیء ایجاد شده‌ی توسط useLocalStore، محلی و محدود به طول عمر کامپوننت تابعی تعریف شده‌است.
- در اینجا شیء بازگشت داده شده‌ی توسط useLocalStore، دارای دو خاصیت title و done است. این دو خاصیت بدون نیاز به هیچ تعریف خاصی، observable در نظر گرفته می‌شوند. Fi به علاوه خاصیت getter آن به نام emoji نیز به عنوان یک خاصیت محاسباتی MobX تفسیر شده و متد toggle آن به صورت یک action پردازش می‌شود. بنابراین در حین کار با MobX Hooks دیگر نیازی به تغییر ساختار پروژه‌ی React، برای پشتیبانی از decorators نیست.
- در این مثال، return useObserver را نیز مشاهده می‌کنید. کار آن رندر مجدد کامپوننت، با تغییر یکی از خواص observable ردیابی شده‌ی توسط آن است.


امکان تعریف global state با کمک useLocalStore

نام useLocalStore از این جهت انتخاب شده‌است که مشخص کند مخزن حالت ایجاد شده‌ی توسط آن، درون یک کامپوننت به صورت محلی ایجاد می‌شود و سراسری نیست. اما این نکته به این معنا نیست که نمی‌توان مخزن حالت ایجاد شده‌ی توسط آن‌را در بین سلسه مراتب کامپوننت‌های برنامه به اشتراک گذاشت. توسط تابع useLocalStore می‌توان چندین مخزن حالت را ایجاد کرد و سپس توسط شیءای دیگر آن‌ها را یکی کرده و در آخر به کمک Context API خود React آن‌را در اختیار تمام کامپوننت‌های برنامه قرار داد.

تا نگارش MobX 5x (و همچنین پس از آن)، توسط inject@ می‌توان یک مخزن حالت را در اختیار یک کامپوننت قرار داد (مانند inject('myStore')). طراحی inject@ مربوط است به زمانیکه امکان دسترسی به Context پشت صحنه‌ی React به صورت عمومی توسط Context API آن ارائه نشده بود. به همین جهت از این پس دیگر نیازی به استفاده‌ی از آن نیست.


چگونه توسط MobX Hooks، یک مخزن حالت سراسری را ایجاد کنیم؟

برای ایجاد یک مخزن حالت سراسری با روش جدید MobX Hooks، مراحل زیر را می‌توان طی کرد:

الف) ایجاد شیء store
ابتدا متدی را مانند createStore ایجاد می‌کنیم، به نحوی که یک شیء را بازگشت دهد. این شیء همانطور که عنوان شد، خواصش، getters و متدهای آن، توسط MobX ردیابی خواهند شد (مانند const todo = useLocalStore مثال فوق) و نیازی به اعمال MobX Decorators را ندارند.
export function createStore() {
  return {
   // ...
  }
}

ب) برپایی Context
اینبار دیگر نه از شیء Provider خود MobX استفاده می‌کنیم و نه از تزئین کننده‌ی inject@ آن؛ بلکه از React Context استاندارد استفاده خواهیم کرد:
import React from 'react';
import { createStore } from './createStore';
import { useLocalStore } from 'mobx-react'; // 6.x or mobx-react-lite@1.4.0

const storeContext = React.createContext(null);

export const StoreProvider = ({ children }) => {
  const store = useLocalStore(createStore);
  return <storeContext.Provider value={store}>{children}</storeContext.Provider>;
}

export const useStore = () => {
  const store = React.useContext(storeContext);
  if (!store) {
    throw new Error('useStore must be used within a StoreProvider.');
  }
  return store
}
- در اینجا فرض شده‌است که تابع createStore که شیء store ما را ارائه می‌دهد از ماژولی به نام createStore دریافت می‌شود.
- سپس توسط React.createContext، یک شیء Context استاندارد React را ایجاد می‌کنیم؛ به نام storeContext.
- تابع کمکی StoreProvider، جایگزین شیء Provider قبلی MobX می‌شود. یعنی کارش محصور کردن کامپوننت App برنامه است تا شیء store را در اختیار سلسه مراتب کامپوننت‌های React قرار دهد. در اینجا children به همان کامپوننت‌هایی که قرار است توسط Context.Provider محصور شوند اشاره می‌کند.
- تابع کمکی useStore، جهت محصور کردن  متد React.useContext، اضافه شده‌است. می‌توانید useContext Hook را به صورت مستقیم در کامپوننت‌های تابعی فراخوانی کنید و یا می‌توانید از متد کمکی useStore بجای آن استفاده نمائید تا حجم کدهای تکراری برنامه کاهش یابد.

ج) استفاده‌ی از StoreProvider تهیه شده
اکنون با استفاده از متد StoreProvider فوق که شیء Context.Provider استاندارد React را بازگشت می‌دهد، می‌توان کامپوننت‌های بالاترین کامپوننت سلسه مراتب کامپوننت‌های برنامه را محصور کرد، تا تمام آن‌ها بتوانند به store ذخیره شده‌ی در Provider، دسترسی پیدا کنند:
export default function App() {
  return (
    <StoreProvider>
      <main>
        <Component1 />
        <Component2 />
        <Component3 />
      </main>
    </StoreProvider>
  );
}

د) استفاده از store مهیا شده در کامپوننت‌های تابعی برنامه
پس از تهیه‌ی متدی کمکی useStore که در حقیقت همان useContext Hook است، می‌توان به کمک آن در کامپوننت‌های تابعی، به store و تمام امکانات آن دسترسی پیدا کرد:
const store = useStore();
به این ترتیب دیگر نیازی به inject@ نخواهد بود.

سؤال: آیا هنوز هم می‌توان یک مخزن پیچیده‌ی متشکل از چندین کلاس را تشکیل داد؟
پاسخ: بله. برای مثال ابتدا دو کلاس جدید CounterStore و ThemeStore را به نحو متداولی، با استفاده‌ی از MobX decorators طراحی می‌کنیم (دقیقا مانند مثال قسمت قبل). سپس بجای ذکر نال، بجای پارامتر متد createContext، آن‌را با یک شیء جدید مقدار دهی می‌کنیم که هر کدام از خواص آن، به یک وهله از مخازن حالت ایجاد شده اشاره می‌کند:
export const storesContext = React.createContext({
  counterStore: new CounterStore(),
  themeStore: new ThemeStore(),
});

export const useStores = () => React.useContext(storesContext);
با این تعییر اگر در کامپوننتی از برنامه نیاز به برای مثال شیء منتسب به خاصیت counterStore را داشتیم، می‌توان به صورت زیر عمل کرد:
const { counterStore } = useStores();


چند نکته‌ی تکمیلی

نکته 1: با اشیاء MobX از Object Destructuring استفاده نکنید!

اگر بر روی اشیاء MobX از Object Destructuring استفاده کنیم، خروجی آن تبدیل به متغیرهای ساده‌ای خواهند شد که دیگر ردیابی نمی‌شوند.
برای مثال اگر counterStore مثال فوق به همراه خاصیت observable ای به نام activeUserName است، آن‌را به صورت زیر تبدیل به متغیر activeUserName نکنید؛ چون دیگر reactive نخواهد بود:
const {
    counterStore: { activeUserName },
} = useStores();
فقط بالاترین سطح مخزن را به صورت زیر توسط Object Destructuring از آن استخراج و سپس استفاده کنید:
const { counterStore } = useStores();


نکته 2: مدیریت side effects با MobX

در مورد اثرات جانبی و side effects در مطلب «قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور» بیشتر بحث شد. اگر یک اثر جانبی مانند تنظیم document.title، به مقدار یک خاصیت observable وابسته بود، می‌توان از متد autorun که تغییرات آن‌ها را ردیابی می‌کند، درون useEffect Hook استاندارد، استفاده کرد:
import React from 'react'
import { autorun } from 'mobx'

function useDocumentTitle(store) {
  React.useEffect(
    () =>
      autorun(() => {
        document.title = `${store.title} - ${store.sectionName}`
      }),
    [], // note empty dependencies
  )
}
در حین کار با MobX، هیچگاه نیازی به ذکر وابستگی‌های تابع useEffect نیست؛ چون اساسا وجود خارجی ندارند و توسط خود MobX مدیریت می‌شوند و به store وابسته‌اند و نه به حالت کامپوننت جاری.


نکته 4: روش فعالسازی MobX strict mode

اگر strict mode را در Mobx به روش زیر فعال کنیم:
import { configure } from "mobx";
configure({ enforceActions: true });
پس از آن باید حالت مدیریت شده‌ی توسط MobX را فقط و فقط توسط action‌های آن تغییر داد و اگر سعی در تغییر مقدار مستقیم یک خاصیت observable کنیم، استثنایی صادر خواهد شد. برای تغییر خواص observable باید آن‌ها را درون یک action قرار داد؛ تا مطابق رهنمودهای طراحی کلاس‌های MobX باشد.


نکته 3: روش انجام اعمال async در MobX

فرض کنید یک عملیات async را در یک اکشن متد کلاس حالت MobX، به صورت زیر انجام داده‌ایم و نتیجه‌ی آن به خاصیت weatherData آن کلاس که observable است، به صورت مستقیم انتساب داده شده‌است:
@action
loadWeather = city => {
  fetch(
    `https://abnormal-weather-api.herokuapp.com/cities/search?city=${city}`
  )
    .then(response => response.json())
    .then(data => {
      this.weatherData = data;
    });
};
هرچند loadWeather یک متد را ارائه می‌دهد که به صورت action معرفی شده‌است، اما هرچیزی که داخل آن قرار می‌گیرد، الزاما تحت کنترل آن نیست. برای مثال متد then، یک تابع callback جدید را فراخوانی می‌کند که اعمال آن، تحت کنترل loadWeather نیست. به همین جهت اگر strict mode را فعال کرده باشیم، عنوان می‌کند که خواص observable را باید درون یک اکشن متد تغییر داد و نه به صورت مستقیم؛ مانند this.weatherData در اینجا.

راه حل اول: تغییر خاصیت this.weatherData را به یک اکشن متد مجزا انتقال می‌دهیم:
@action setWeather = data => {
    this.weatherData = data;
};
اکنون می‌توان قسمت then را به صورت then(data => this.setWeather(data)) نوشت و خطای یاد شده برطرف می‌شود.

راه حل دوم: اگر نمی‌خواهیم یک اکشن متد جدید را تعریف کنیم، می‌توان از متد کمکی runInAction در داخل یک callback استفاده کرد:
  loadWeatherInline = city => {
    fetch(`http://jsonplaceholder.typicode.com/comments/${city}`)
      .then(response => response.json())
      .then(data => {
        runInAction(() => (this.weatherData = data));
      });
  };
runInAction یکی از متدهای قابل دریافت از mobx است.

در مورد اعمال async/await چطور؟
در اینجا هم تفاوتی نمی‌کند. هر چیزی پس از await، شبیه به حالت متد then پردازش می‌شود. به همین جهت در اینجا نیز باید از یکی از دو راه حل ارائه شده، استفاده کرد:
  loadWeatherAsync = async city => {
    const response = await fetch(
      `http://jsonplaceholder.typicode.com/comments/${city}`
    );
    const data = await response.json();
    runInAction(() => {
      this.weatherData = data;
    });
  };
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 4 - طول حیات سرویس ها یا Service Lifetime
در قسمت‌های قبلی این سری، به ترتیب ابتدا در مورد مبحث تزریق وابستگی‌ها صبحت کردیم، بعد اولین سرویس‌مان را در ASP.NET Core ثبت و واکشی کردیم. در بخش سوم، تنظیمات را درون سامانه، ثبت و استفاده کردیم و حالا در این بخش می‌خواهیم به مبحث طول حیات سرویس‌ها بپردازیم.
همانطور که گفتیم، وظیفه‌ی DI Container، ایجاد یک نمونه از سرویس درخواست شده، تزریق آن به کلاس درخواست دهنده و در انتها از بین بردن یا Dispose شیء ایجاد شده از سرویس ثبت شده‌است. بنابراین ما باید در هنگام ثبت سرویس، بر اساس تحلیل و نیاز برنامه‌ی خودمان، طول عمر سرویس/Service Life Time را مشخص کنیم.

بصورت کلی در Microsoft Dependency Injection Container و اکثر DI Container‌های دیگر، 3 نوع کلی چرخه‌ی حیات وجود دارند که به ترتیب پایداری و طول عمر شیء ایجاد شده، در زیر آورده شده‌اند:
  •  Singleton
  •  Scoped
  •  Transient

Singleton (یگانه)

فقط و فقط یک شیء از سرویس ثبت شده با این طول عمر، در اولین درخواست ایجاد می‌شود و سپس در کل طول حیات برنامه، از همین شیء ایجاد شده، استفاده می‌گردد.  به همین دلیل به آن «یگانه» یا Singleton می‌گویند. هر زمانیکه این سرویس در خواست داده می‌شود، DI Container، همان یک شیء را در اختیار درخواست دهنده قرار می‌دهد و این شیء، هیچگاه از بین نمی‌رود.  به بیان دیگر، DI Container هیچگاه این شیء را از بین نمی‌برد. شیء ساخته شده از سرویس ثبت شده‌ی با حالت Singleton، بین تمامی استفاده کنندگان، به صورت اشتراکی استفاده می‌شود. این طول عمر تقریبا مشابه‌ی اشیاء ساخته شده توسط Singleton Pattern عمل می‌کند.
با توجه به مطالب گفته شده، ویژگی‌های سرویس‌های Singleton به شرح زیر هستند:
  •   در اولین درخواست به سرویس، یک نمونه از آن ساخته می‌شود و تا پایان برنامه در حافظه نگه داشته می‌شود.
  •   در سایر درخواست‌ها، همان یک نمونه‌ی ساخته شده‌ی از سرویس، ارائه داده می‌شود. 
  •   به علت موجود بودن در حافظه، معمولا دسترسی به آن‌ها و عملکرد آن‌ها سریعتر است.
  •   بار کاری بر روی Garbage Collector فریمورک را کاهش می‌دهند.

بنابراین در هنگام تعریف کردن یک سرویس به صورت Singleton باید نکات زیر را مد نظر قرار بدهید:
  • باید سرویس مورد نظر Thread Safe باشد .
  •  نباید استفاده کننده‌ی از این سرویس، امکان تغییر State آن را داشته باشد.
  •  اگر ساخت شیء‌ای از یک سرویس، هزینه‌ی زیادی را داشته باشد ، احتمالا Singleton کردن آن می‌تواند ایده‌ی خوبی باشد.
  •  شیء ساخته شده‌ی از این سرویس، تا زمان اجرای برنامه، بخشی از حافظه‌ی برنامه را اشغال می‌کند. پس باید حجم اشغالی در حافظه را نیز مد نظر قرار داد.
  •  تعداد دفعات استفاده را در برابر مصرف حافظه در نظر بگیرید.
معمولا سرویس‌هایی مثل تنظیمات برنامه، از این نوع تعریف می‌شوند.

برای ثبت یک سرویس به صورت Singleton می‌توانیم از متدهای توسعه‌ای با نام ()AddSingleton، با سربارهای مختلف بر روی IServiceCollection استفاده کنیم. علاوه بر این، در هنگام استفاده از Option Pattern، متد Configure، خودش سرویس مورد نظر را به صورت Singleton ثبت می‌کند.

خب، به روش زیر سرویس GuidProvider را بعنوان یک Singleton  تعریف می‌کنیم:
services.AddSingleton(services => new GuidProvider());
 اکنون این سرویس را درون اکشن Index  و کنترلر HomeController تزریق می‌کنیم:
        public HomeController(ILogger<HomeController> logger,
            IMessageServiceA messageService,
            LiteDbConfig liteDbConfig,
            GuidProvider guidHelper)
        {
            _logger = logger;
            _messageService = messageService;
            _messageService = new MessageServiceAA();
            _guidHelper = guidHelper;
        }

حالا اگر برنامه را اجرا کنیم، می‌بینید که با تازه سازی صفحه‌ی Home/Index ، همچنان Id، برابر با یک رشته‌ی یکسان است. حتی اگر تب دیگری را در مرورگر باز کنیم و دوباره به این صفحه برویم، می‌بینیم که Id برابر همان رشته‌ی قبلی است و دلیل این موضوع، ثبت سرویس Guid Service به صورت Singleton است.


Scoped ( محدود شده )

به ازای هر درخواست (در اینجا معمولا درخواست‌های Http مد نظر است) یک نمونه از این سرویس ساخته می‌شود و در طول حیات این درخواست، DI Container به هر کلاسی که به این سرویس نیاز دارد، همان یک نمونه را برگشت می‌دهد و این نمونه در کل طول اجرای این درخواست، بین تمامی سرویس گیرندگان، یکسان است. هر زمانی، درخواست به پایان برسد، نمونه‌ی ساخته شده از سرویس، Disposed می‌گردد و GC می‌تواند آن را از بین ببرد.

معمولا سرویس‌های اتصال به پایگاه داده‌ها و کار بر روی آنها که شامل خواندن، نوشتن، ویرایش، حذف می‌شوند را با طول حیات Scoped ، درون DI Container ثبت می‌کنند . EF Core به صورت پیش فرض ، Db Context را به صورت Scoped ثبت می‌کند.

سرویس‌های Scoped در محدوده‌ی درخواست، مانند  Singleton عمل می‌کنند و شیء ساخته شده و وضعیت آن در بین تمامی سرویس‌هایی  که به آن نیاز دارند، مشترک است. بنابراین باید به این نکته در هنگام تعریف سرویس به صورت Scoped ، توجه داشته باشید.

تمام Middleware ‌های ASP.NET Core هم فقط همان نمونه‌ی ایجاد شده از سرویس Scoped را در طی اجرای یک درخواست خاص، می‌گیرند.

هر سرویسی که به سرویس‌های Scoped نیاز دارد، یا باید به صورت Transient و یا باید به صورت Scoped ثبت شود، تا مانع از این شویم که شیء ساخته شده، فراتر از طول حیات موردنظرمان، در حافظه بماند و از آن استفاده شود .

برای ثبت یک سرویس به صورت Scoped می‌توانیم از متدهای توسعه‌ای با نام AddScoped() با سربارهای مختلف بر روی IServiceCollection استفاده کنیم. در اینجا از نسخه‌ای که دو پارامتر جنریک را می‌گیرد، برای ثبت یک سرویس به صورت Scoped استفاده می‌کنیم:

services.AddScoped<IMessageServiceB, MessageServiceBA>();

می توانیم سرویس GuidProvider را  به جای Signleton ، به صورت Scoped ثبت کنیم: 

services.AddScoped(services => new GuidProvider());
حال اگر برنامه را اجرا کنیم، می بینید که این بار با تازه سازی صفحه‌ی Home/Index، مقدار  Id برابر با یک رشته‌ی جدید است.  

 

Transient (گذرا)

به ازای هر درخواست دهنده‌ی جدید، یک نمونه‌ی جدید از سرویس، توسط DI Container ساخته می‌شود و در اختیار آن قرار می‌گیرد.

سرویس‌هایی را به این صورت ثبت کنید که:

  •   نیاز به Thread Safe بودن داشته باشند.
  • نمی توانید طول عمر سرویس را حدس بزنید.

سرویس‌های Transient ، کارآیی پائین‌تری دارند و سربار عملکردی زیادی بر روی Garbage Collector می گذارند؛ ولی به علت اینکه به ازای هر واکشی، یک نمونه‌ی جدید از آن‌ها ساخته می‌شود و State بین این اشیاء به اشتراک گذاشته نمی‌شود، امنیت بیشتری دارند و درک و خطایابی آنها ساده‌تر است.

برای ثبت سرویس‌های Transient از متد توسعه‌ای AddTransient() استفاده می‌کنیم. سربارهای این متد مانند سربارهای متدهای AddSingleton() و AddScoped() است:

services.AddTransient<IMessageServiceC, MessageServiceCA>();

 

وابستگی‌های محصور شده

یک سرویس نباید وابسته‌ی به سرویسی باشد که طول حیاتی کمتر از طول حیات خودش را دارد.

برای مثال اگر درون سرویسی با طول حیات Singleton، از یک سرویس با طول حیات Transient استفاده کنیم، اینکار باعث می‌شود که یک نمونه از سرویس Transient در طول حیات برنامه، همیشه درون حافظه بماند و این ممکن است باعث خطاهای عجیبی در هنگام اجرا شود که معمولا خطایابی و رفع آن‌ها مشکل است.


اثرات جانبی وابستگی‌های محصور شده:

  • به اشتراک گذاری تصادفی وضعیت یک شیء بین Thread ‌ها درون سرویس‌هایی که Thread Safe نیستند.
  • اشیاء، بیش از زمان پیش بینی شده‌ی برایشان، درون حافظه باقی می‌مانند.


سرویس‌های Transient می‌توانند به سرویس‌هایی با طول حیات زیر وابستگی داشته باشند:

  •   Transient
  •   Scoped
  •   Singleton

 

سرویس‌های Scoped می‌توانند به سرویس‌هایی با طول حیات زیر وابستگی داشته باشند:

  • Transient
  •   Scoped


سرویس‌های Singleton می‌توانند به سرویس هایی با طول حیات زیر وابستگی داشته باشند:

Singleton  


می‌توانید از جدول زیر به عنوان راهنمای خلاصه شده‌ی برای استفاده‌ی امن از سرویس‌ها درون یکدیگر بهره ببرید:


Scope Validation 

این قابلیت که به صورت پیش فرض در حالت توسعه‌ی برنامه‌های ASP.NET Core فعال است، در زمان شروع برنامه و در Startup ، بررسی می‌کند که سرویس‌ها، وابستگی به سرویس‌هایی با طول حیاتی مناسب، داشته باشند.

مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت دوم
در قسمت قبل معماری اپلیکیشن‌های N-Tier و بروز رسانی موجودیت‌های منفصل توسط Web API را بررسی کردیم. در این قسمت بروز رسانی موجودیت‌های منفصل توسط WCF را بررسی می‌کنیم.

بروز رسانی موجودیت‌های منفصل توسط WCF

سناریویی را در نظر بگیرید که در آن عملیات CRUD توسط WCF پیاده سازی شده اند و دسترسی داده‌ها با مدل Code-First انجام می‌شود. فرض کنید مدل اپلیکیشن مانند تصویر زیر است.

همانطور که می‌بینید مدل ما متشکل از پست‌ها و نظرات کاربران است. برای ساده نگاه داشتن مثال جاری، اکثر فیلدها حذف شده اند. مثلا متن پست ها، نویسنده، تاریخ و زمان انتشار و غیره. می‌خواهیم تمام کد دسترسی داده‌ها را در یک سرویس WCF پیاده سازی کنیم تا کلاینت‌ها بتوانند عملیات CRUD را توسط آن انجام دهند. برای ساختن این سرویس مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع Class Library بسازید و نام آن را به Recipe2 تغییر دهید.
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • سه کلاس با نام‌های Post, Comment و Recipe2Context به پروژه اضافه کنید. کلاس‌های Post و Comment موجودیت‌های مدل ما هستند که به جداول متناظرشان نگاشت می‌شوند. کلاس Recipe2Context آبجکت DbContext ما خواهد بود که بعنوان درگاه عملیاتی EF عمل می‌کند. دقت کنید که خاصیت‌های لازم WCF یعنی DataContract و DataMember در کلاس‌های موجودیت‌ها بدرستی استفاده می‌شوند. لیست زیر کد این کلاس‌ها را نشان می‌دهد.
[DataContract(IsReference = true)]
public class Post
{
    public Post()
    {
        comments = new HashSet<Comments>();
    }
    
    [DataMember]
    public int PostId { get; set; }
    [DataMember]
    public string Title { get; set; }
    [DataMember]
    public virtual ICollection<Comment> Comments { get; set; }
}

[DataContract(IsReference=true)]
public class Comment
{
    [DataMember]
    public int CommentId { get; set; }
    [DataMember]
    public int PostId { get; set; }
    [DataMember]
    public string CommentText { get; set; }
    [DataMember]
    public virtual Post Post { get; set; }
}

public class EFRecipesEntities : DbContext
{
    public EFRecipesEntities() : base("name=EFRecipesEntities") {}

    public DbSet<Post> posts;
    public DbSet<Comment> comments;
}
  • یک فایل App.config به پروژه اضافه کنید و رشته اتصال زیر را به آن اضافه نمایید.
<connectionStrings>
  <add name="Recipe2ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • حال یک پروژه WCF به Solution جاری اضافه کنید. برای ساده نگاه داشتن مثال جاری، نام پیش فرض Service1 را بپذیرید. فایل IService1.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید.
[ServiceContract]
public interface IService1
{
    [OperationContract]
    void Cleanup();
    [OperationContract]
    Post GetPostByTitle(string title);
    [OperationContract]
    Post SubmitPost(Post post);
    [OperationContract]
    Comment SubmitComment(Comment comment);
    [OperationContract]
    void DeleteComment(Comment comment);
}
  • فایل Service1.svc.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. بیاد داشته باشید که پروژه Recipe2 را ارجاع کنید و فضای نام آن را وارد نمایید. همچنین کتابخانه EF 6 را باید به پروژه اضافه کنید.
public class Service1 : IService
{
    public void Cleanup()
    {
        using (var context = new EFRecipesEntities())
        {
            context.Database.ExecuteSqlCommand("delete from [comments]");
            context. Database.ExecuteSqlCommand ("delete from [posts]");
        }
    }

    public Post GetPostByTitle(string title)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Configuration.ProxyCreationEnabled = false;
            var post = context.Posts.Include(p => p.Comments).Single(p => p.Title == title);
            return post;
        }
    }

    public Post SubmitPost(Post post)
    {
        context.Entry(post).State =
            // if Id equal to 0, must be insert; otherwise, it's an update
            post.PostId == 0 ? EntityState.Added : EntityState.Modified;
        context.SaveChanges();
        return post;
    }

    public Comment SubmitComment(Comment comment)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Comments.Attach(comment);
            if (comment.CommentId == 0)
            {
                // this is an insert
                context.Entry(comment).State = EntityState.Added);
            }
            else
            {
                // set single property to modified, which sets state of entity to modified, but
                // only updates the single property – not the entire entity
                context.entry(comment).Property(x => x.CommentText).IsModified = true;
            }
            context.SaveChanges();
            return comment;
        }
    }

    public void DeleteComment(Comment comment)
    {
        using (var context = new EFRecipesEntities())
        {
            context.Entry(comment).State = EntityState.Deleted;
            context.SaveChanges();
        }
    }
}


  • در آخر پروژه جدیدی از نوع Windows Console Application به Solution جاری اضافه کنید. از این اپلیکیشن بعنوان کلاینتی برای تست سرویس WCF استفاده خواهیم کرد. فایل program.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. روی نام پروژه کلیک راست کرده و گزینه Add Service Reference را انتخاب کنید، سپس ارجاعی به سرویس Service1 اضافه کنید. رفرنسی هم به کتابخانه کلاس‌ها که در ابتدای مراحل ساختید باید اضافه کنید.
class Program
{
    static void Main(string[] args)
    {
        using (var client = new ServiceReference2.Service1Client())
        {
            // cleanup previous data
            client.Cleanup();
            // insert a post
            var post = new Post { Title = "POCO Proxies" };
            post = client.SubmitPost(post);
            // update the post
            post.Title = "Change Tracking Proxies";
            client.SubmitPost(post);
            // add a comment
            var comment1 = new Comment { CommentText = "Virtual Properties are cool!", PostId = post.PostId };
            var comment2 = new Comment { CommentText = "I use ICollection<T> all the time", PostId = post.PostId };
            comment1 = client.SubmitComment(comment1);
            comment2 = client.SubmitComment(comment2);
            // update a comment
            comment1.CommentText = "How do I use ICollection<T>?";
            client.SubmitComment(comment1);
            // delete comment 1
            client.DeleteComment(comment1);
            // get posts with comments
            var p = client.GetPostByTitle("Change Tracking Proxies");
            Console.WriteLine("Comments for post: {0}", p.Title);
            foreach (var comment in p.Comments)
            {
                Console.WriteLine("\tComment: {0}", comment.CommentText);
            }
        }
    }
}
اگر اپلیکیشن کلاینت (برنامه کنسول) را اجرا کنید با خروجی زیر مواجه می‌شوید.

Comments for post: Change Tracking Proxies
Comment: I use ICollection<T> all the time


شرح مثال جاری

ابتدا با اپلیکیشن کنسول شروع می‌کنیم، که کلاینت سرویس ما است. نخست در یک بلاک {} using وهله ای از کلاینت سرویس مان ایجاد می‌کنیم. درست همانطور که وهله ای از یک EF Context می‌سازیم. استفاده از بلوک‌های using توصیه می‌شود چرا که متد Dispose بصورت خودکار فراخوانی خواهد شد، چه بصورت عادی چه هنگام بروز خطا. پس از آنکه وهله ای از کلاینت سرویس را در اختیار داشتیم، متد Cleanup را صدا می‌زنیم. با فراخوانی این متد تمام داده‌های تست پیشین را حذف می‌کنیم. در چند خط بعدی، متد SubmitPost را روی سرویس فراخوانی می‌کنیم. در پیاده سازی فعلی شناسه پست را بررسی می‌کنیم. اگر مقدار شناسه صفر باشد، خاصیت State موجودیت را به Added تغییر می‌دهید تا رکورد جدیدی ثبت کنیم. در غیر اینصورت فرض بر این است که چنین موجودیتی وجود دارد و قصد ویرایش آن را داریم، بنابراین خاصیت State را به Modified تغییر می‌دهیم. از آنجا که مقدار متغیرهای int بصورت پیش فرض صفر است، با این روش می‌توانیم وضعیت پست‌ها را مشخص کنیم. یعنی تعیین کنیم رکورد جدیدی باید ثبت شود یا رکوردی موجود بروز رسانی گردد. رویکردی بهتر آن است که پارامتری اضافی به متد پاس دهیم، یا متدی مجزا برای ثبت رکوردهای جدید تعریف کنیم. مثلا رکوردی با نام InsertPost. در هر حال، بهترین روش بستگی به ساختار اپلیکیشن شما دارد.

اگر پست جدیدی ثبت شود، خاصیت PostId با مقدار مناسب جدید بروز رسانی می‌شود و وهله پست را باز می‌گردانیم. ایجاد و بروز رسانی نظرات کاربران مشابه ایجاد و بروز رسانی پست‌ها است، اما با یک تفاوت اساسی: بعنوان یک قانون، هنگام بروز رسانی نظرات کاربران تنها فیلد متن نظر باید بروز رسانی شود. بنابراین با فیلدهای دیگری مانند تاریخ انتشار و غیره اصلا کاری نخواهیم داشت. بدین منظور تنها خاصیت CommentText را بعنوان Modified علامت گذاری می‌کنیم. این امر منجر می‌شود که Entity Framework عبارتی برای بروز رسانی تولید کند که تنها این فیلد را در بر می‌گیرد. توجه داشته باشید که این روش تنها در صورتی کار می‌کند که بخواهید یک فیلد واحد را بروز رسانی کنید. اگر می‌خواستیم فیلدهای بیشتری را در موجودیت Comment بروز رسانی کنیم، باید مکانیزمی برای ردیابی تغییرات در سمت کلاینت در نظر می‌گرفتیم. در مواقعی که خاصیت‌های متعددی می‌توانند تغییر کنند، معمولا بهتر است کل موجودیت بروز رسانی شود تا اینکه مکانیزمی پیچیده برای ردیابی تغییرات در سمت کلاینت پیاده گردد. بروز رسانی کل موجودیت بهینه‌تر خواهد بود.

برای حذف یک دیدگاه، متد Entry را روی آبجکت DbContext فراخوانی می‌کنیم و موجودیت مورد نظر را بعنوان آرگومان پاس می‌دهیم. این امر سبب می‌شود که موجودیت مورد نظر بعنوان Deleted علامت گذاری شود، که هنگام فراخوانی متد SaveChanges اسکریپت لازم برای حذف رکورد را تولید خواهد کرد.

در آخر متد GetPostByTitle یک پست را بر اساس عنوان پیدا کرده و تمام نظرات کاربران مربوط به آن را هم بارگذاری می‌کند. از آنجا که ما کلاس‌های POCO را پیاده سازی کرده ایم، Entity Framework آبجکتی را بر می‌گرداند که Dynamic Proxy نامیده می‌شود. این آبجکت پست و نظرات مربوط به آن را در بر خواهد گرفت. متاسفانه WCF نمی‌تواند آبجکت‌های پروکسی را مرتب سازی (serialize) کند. اما با غیرفعال کردن قابلیت ایجاد پروکسی‌ها (ProxyCreationEnabled=false) ما به Entity Framework می‌گوییم که خود آبجکت‌های اصلی را بازگرداند. اگر سعی کنید آبجکت پروکسی را سریال کنید با پیغام خطای زیر مواجه خواهید شد:

The underlying connection was closed: The connection was closed unexpectedly 

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

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

مطالب
UML و VS2010
مطالب
ASP.NET MVC #13

اعتبار سنجی اطلاعات ورودی در فرم‌های ASP.NET MVC

زمانیکه شروع به دریافت اطلاعات از کاربران کردیم، نیاز خواهد بود تا اعتبار اطلاعات ورودی را نیز ارزیابی کنیم. در ASP.NET MVC، به کمک یک سری متادیتا، نحوه‌ی اعتبار سنجی، تعریف شده و سپس فریم ورک بر اساس این ویژگی‌ها، به صورت خودکار اعتبار اطلاعات انتساب داده شده به خواص یک مدل را در سمت کلاینت و همچنین در سمت سرور بررسی می‌نماید.
این ویژگی‌ها در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند که به صورت پیش فرض در هر پروژه جدید ASP.NET MVC لحاظ می‌شود.

یک مثال کاربردی

مدل زیر را به پوشه مدل‌های یک پروژه جدید خالی ASP.NET MVC اضافه کنید:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Models
{
public class Customer
{
public int Id { set; get; }

[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
public string Name { set; get; }

[Display(Name = "Email address")]
[Required(ErrorMessage = "Email address is required.")]
[RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*",
ErrorMessage = "Please enter a valid email address.")]
public string Email { set; get; }

[Range(0, 10)]
[Required(ErrorMessage = "Rating is required.")]
public double Rating { set; get; }

[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
public DateTime StartDate { set; get; }
}
}

سپس کنترلر جدید زیر را نیز به برنامه اضافه نمائید:
using System.Web.Mvc;
using MvcApplication9.Models;

namespace MvcApplication9.Controllers
{
public class CustomerController : Controller
{
[HttpGet]
public ActionResult Create()
{
var customer = new Customer();
return View(customer);
}

[HttpPost]
public ActionResult Create(Customer customer)
{
if (this.ModelState.IsValid)
{
//todo: save data
return Redirect("/");
}
return View(customer);
}
}
}

بر روی متد Create کلیک راست کرده و گزینه Add view را انتخاب کنید. در صفحه باز شده، گزینه Create a strongly typed view را انتخاب کرده و مدل را Customer انتخاب کنید. همچنین قالب Scaffolding را نیز بر روی Create قرار دهید.

توضیحات تکمیلی

همانطور که در مدل برنامه ملاحظه می‌نمائید، به کمک یک سری متادیتا یا اصطلاحا data annotations، تعاریف اعتبار سنجی، به همراه عبارات خطایی که باید به کاربر نمایش داده شوند، مشخص شده است. ویژگی Required مشخص می‌کند که کاربر مجبور است این فیلد را تکمیل کند. به کمک ویژگی StringLength، حداکثر تعداد حروف قابل قبول مشخص می‌شود. با استفاده از ویژگی RegularExpression، مقدار وارد شده با الگوی عبارت باقاعده مشخص گردیده، مقایسه شده و در صورت عدم تطابق، پیغام خطایی به کاربر نمایش داده خواهد شد. به کمک ویژگی Range، بازه اطلاعات قابل قبول، مشخص می‌گردد.
ویژگی دیگری نیز به نام System.Web.Mvc.Compare مهیا است که برای مقایسه بین مقادیر دو خاصیت کاربرد دارد. برای مثال در یک فرم ثبت نام، عموما از کاربر درخواست می‌شود که کلمه عبورش را دوبار وارد کند. ویژگی Compare در یک چنین مثالی کاربرد خواهد داشت.
در مورد جزئیات کنترلر تعریف شده در قسمت 11 مفصل توضیح داده شد. برای مثال خاصیت this.ModelState.IsValid مشخص می‌کند که آیا کارmodel binding موفق بوده یا خیر و همچنین اعتبار سنجی‌های تعریف شده نیز در اینجا تاثیر داده می‌شوند. بنابراین بررسی آن پیش از ذخیره سازی اطلاعات ضروری است.
در حالت HttpGet صفحه ورود اطلاعات به کاربر نمایش داده خواهد شد و در حالت HttpPost، اطلاعات وارد شده دریافت می‌گردد. اگر دست آخر، ModelState معتبر نبود، همان اطلاعات نادرست وارد شده به کاربر مجددا نمایش داده خواهد شد تا فرم پاک نشود و بتواند آن‌ها را اصلاح کند.
برنامه را اجرا کنید. با مراجعه به مسیر http://localhost/customer/create، صفحه ورود اطلاعات کاربر نمایش داده خواهد شد. در اینجا برای مثال در قسمت ورود اطلاعات آدرس ایمیل، مقدار abc را وارد کنید. بلافاصله خطای اعتبار سنجی عدم اعتبار مقدار ورودی نمایش داده می‌شود. یعنی فریم ورک، اعتبار سنجی سمت کاربر را نیز به صورت خودکار مهیا کرده است.
اگر علاقمند باشید که صرفا جهت آزمایش، اعتبار سنجی سمت کاربر را غیرفعال کنید، به فایل web.config برنامه مراجعه کرده و تنظیم زیر را تغییر دهید:

<appSettings>
<add key="ClientValidationEnabled" value="true"/>

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

@{ Html.EnableClientValidation(false); }

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

نحوه تعریف عناصر مرتبط با اعتبار سنجی در Viewهای برنامه نیز به شکل زیر است:

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Customer</legend>

<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>

همانطور که ملاحظه می‌کنید به صورت پیش فرض از jQuery validator در سمت کلاینت استفاده شده است. فایل jquery.validate.unobtrusive متعلق به تیم ASP.NET MVC است و کار آن وفق دادن سیستم موجود، با jQuery validator می‌باشد (validation adapter). در نگارش‌های قبلی، از کتابخانه‌های اعتبار سنجی مایکروسافت استفاده شده بود، اما از نگارش سه به بعد، jQuery به عنوان کتابخانه برگزیده مطرح است.
Unobtrusive همچنین در اینجا به معنای مجزا سازی کدهای جاوا اسکریپتی، از سورس HTML صفحه و استفاده از ویژگی‌های data-* مرتبط با HTML5 برای معرفی اطلاعات مورد نیاز اعتبار سنجی است:
<input data-val="true" data-val-required="The Birthday field is required." id="Birthday" name="Birthday" type="text" value="" />

اگر خواستید این مساله را بررسی کنید، فایل web.config قرار گرفته در ریشه اصلی برنامه را باز کنید. در آنجا مقدار UnobtrusiveJavaScriptEnabled را false کرده و بار دیگر برنامه را اجرا کنید. در این حالت کلیه کدهای اعتبار سنجی، به داخل سورس View رندر شده، تزریق می‌شوند و مجزا از آن نخواهند بود.
نحوه‌ی تعریف این اسکریپت‌ها نیز جالب توجه است. متد Url.Content، یک متد سمت سرور می‌باشد که در زمان اجرای برنامه، مسیر نسبی وارد شده را بر اساس ساختار سایت اصلاح می‌کند. حرف ~ بکارگرفته شده، در ASP.NET به معنای ریشه سایت است. بنابراین مسیر نسبی تعریف شده از ریشه سایت شروع و تفسیر می‌شود.
اگر از این متد استفاده نکنیم، مجبور خواهیم شد که مسیرهای نسبی را به شکل زیر تعریف کنیم:

<script src="../../Scripts/customvaildation.js" type="text/javascript"></script>

در این حالت بسته به محل قرارگیری صفحات و همچنین برنامه در سایت، ممکن است آدرس فوق صحیح باشد یا خیر. اما استفاده از متد Url.Content، کار مسیریابی نهایی را خودکار می‌کند.
البته اگر به فایل Views/Shared/_Layout.cshtml، مراجعه کنید، تعریف و الحاق کتابخانه اصلی jQuery در آنجا انجام شده است. بنابراین می‌توان این دو تعریف دیگر مرتبط با اعتبار سنجی را به آن فایل هم منتقل کرد تا همه‌جا در دسترس باشند.
توسط متد Html.ValidationSummary، خطاهای اعتبار سنجی مدل که به صورت دستی اضافه شده باشند نمایش داده می‌شود. این مورد در قسمت 11 توضیح داده شد (چون پارامتر آن true وارد شده، فقط خطاهای سطح مدل را نمایش می‌دهد).
متد Html.ValidationMessageFor، با توجه به متادیتای یک خاصیت و همچنین استثناهای صادر شده حین model binding خطایی را به کاربر نمایش خواهد داد.



اعتبار سنجی سفارشی

ویژگی‌های اعتبار سنجی از پیش تعریف شده، پر کاربردترین‌ها هستند؛ اما کافی نیستند. برای مثال در مدل فوق، StartDate نباید کمتر از سال 2000 وارد شود و همچنین در آینده هم نباید باشد. این موارد اعتبار سنجی سفارشی را چگونه باید با فریم ورک، یکپارچه کرد؟
حداقل دو روش برای حل این مساله وجود دارد:
الف) نوشتن یک ویژگی اعتبار سنجی سفارشی
ب) پیاده سازی اینترفیس IValidatableObject


تعریف یک ویژگی اعتبار سنجی سفارشی

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute
{
public int MinYear { set; get; }

public override bool IsValid(object value)
{
if (value == null) return false;

var date = (DateTime)value;
if (date > DateTime.Now || date < new DateTime(MinYear, 1, 1))
return false;

return true;
}
}
}

برای نوشتن یک ویژگی اعتبار سنجی سفارشی، با ارث بری از کلاس ValidationAttribute شروع می‌کنیم. سپس باید متد IsValid آن‌را تحریف کنیم. اگر این متد false برگرداند به معنای شکست اعتبار سنجی می‌باشد.
در ادامه برای بکارگیری آن خواهیم داشت:
[Display(Name = "Start date")]
[Required(ErrorMessage = "Start date is required.")]
[MyDateValidator(MinYear = 2000,
ErrorMessage = "Please enter a valid date.")]
public DateTime StartDate { set; get; }

اکنون مجددا برنامه را اجرا نمائید. اگر تاریخ غیرمعتبری وارد شود، اعتبار سنجی سمت سرور رخ داده و سپس نتیجه به کاربر نمایش داده می‌شود.


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

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

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using MvcApplication9.CustomValidators;

namespace MvcApplication9.Models
{
public class Customer : IValidatableObject
{
//... same as before

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var fields = new[] { "StartDate" };
if (StartDate > DateTime.Now || StartDate < new DateTime(2000, 1, 1))
yield return new ValidationResult("Please enter a valid date.", fields);

if (Rating > 4 && StartDate < new DateTime(2003, 1, 1))
yield return new ValidationResult("Accepted date should be greater than 2003", fields);
}
}
}

در اینجا در متد Validate، فرصت خواهیم داشت تا به مقادیر کلیه خواص تعریف شده در مدل دسترسی پیدا کرده و بر این اساس اعتبار سنجی بهتری را انجام دهیم. اگر اطلاعات وارد شده مطابق منطق مورد نظر نباشند، کافی است توسط yield return new ValidationResult، یک پیغام را به همراه فیلدهایی که باید این پیغام را نمایش دهند، بازگردانیم.
به این نوع مدل‌ها، self validating models هم گفته می‌شود.


یک نکته:

از MVC3 به بعد، حین کار با ValidationAttribute، امکان تحریف متد IsValid به همراه پارامتری از نوع ValidationContext نیز وجود دارد. به این ترتیب می‌توان به اطلاعات سایر خواص نیز دست یافت. البته در این حالت نیاز به استفاده از Reflection خواهد بود و پیاده سازی IValidatableObject، طبیعی‌تر به نظر می‌رسد:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var info = validationContext.ObjectType.GetProperty("Rating");
//...
return ValidationResult.Success;
}




فعال سازی سمت کلاینت اعتبار سنجی‌های سفارشی

اعتبار سنجی‌های سفارشی تولید شده تا به اینجا، تنها سمت سرور است که فعال می‌شوند. به عبارتی باید یکبار اطلاعات به سرور ارسال شده و در بازگشت، نتیجه عملیات به کاربر نمایش داده خواهد شد. اما ویژگی‌های توکاری مانند Required و Range و امثال آن، علاوه بر سمت سرور، سمت کاربر هم فعال هستند و اگر جاوا اسکریپت در مرورگر کاربر غیرفعال نشده باشد، نیازی به ارسال اطلاعات یک فرم به سرور جهت اعتبار سنجی اولیه، نخواهد بود.
در اینجا باید سه مرحله برای پیاده سازی اعتبار سنجی سمت کلاینت طی شود:
الف) ویژگی سفارشی اعتبار سنجی تعریف شده باید اینترفیس IClientValidatable را پیاده سازی کند.
ب) سپس باید متد jQuery validation متناظر را پیاده سازی کرد.
ج) و همچنین مانند تیم ASP.NET MVC، باید unobtrusive adapter خود را نیز پیاده سازی کنیم. به این ترتیب متادیتای ASP.NET MVC به فرمتی که افزونه jQuery validator آن‌را درک می‌کند، وفق داده خواهد شد.

در ادامه، تکمیل کلاس سفارشی MyDateValidator را ادامه خواهیم داد:
using System;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
using System.Collections.Generic;

namespace MvcApplication9.CustomValidators
{
public class MyDateValidator : ValidationAttribute, IClientValidatable
{
// ... same as before

public IEnumerable<ModelClientValidationRule> GetClientValidationRules(
ModelMetadata metadata,
ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ValidationType = "mydatevalidator",
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName())
};
yield return rule;
}
}
}

در اینجا نحوه پیاده سازی اینترفیس IClientValidatable را ملاحظه می‌نمائید. ValidationType، نام متدی خواهد بود که در سمت کلاینت، کار بررسی اعتبار داده‌ها را به عهده خواهد گرفت.
سپس برای مثال یک فایل جدید به نام customvaildation.js به پوشه اسکریپت‌های برنامه با محتوای زیر اضافه خواهیم کرد:

/// <reference path="jquery-1.5.1-vsdoc.js" />
/// <reference path="jquery.validate-vsdoc.js" />
/// <reference path="jquery.validate.unobtrusive.js" />

jQuery.validator.addMethod("mydatevalidator",
function (value, element, param) {
return Date.parse(value) < new Date();
});

jQuery.validator.unobtrusive.adapters.addBool("mydatevalidator");

توسط referenceهایی که مشاهده می‌کنید، intellisense جی‌کوئری در VS.NET فعال می‌شود.
سپس به کمک متد jQuery.validator.addMethod، همان مقدار ValidationType پیشین را معرفی و در ادامه بر اساس مقدار value دریافتی، تصمیم گیری خواهیم کرد. اگر خروجی false باشد، به معنای شکست اعتبار سنجی است.
همچنین توسط متد jQuery.validator.unobtrusive.adapters.addBool، این متد جدید را به مجموعه وفق دهنده‌ها اضافه می‌کنیم.
و در آخر این فایل جدید باید به View مورد نظر یا فایل master page سیستم اضافه شود:

<script src="@Url.Content("~/Scripts/customvaildation.js")" type="text/javascript"></script>




تغییر رنگ و ظاهر پیغام‌های اعتبار سنجی

اگر از رنگ پیش فرض قرمز پیغام‌های اعتبار سنجی خرسند نیستید، باید اندکی CSS سایت را ویرایش کرد که شامل اعمال تغییرات به موارد ذیل خواهد شد:

1. .field-validation-error
2. .field-validation-valid
3. .input-validation-error
4. .input-validation-valid
5. .validation-summary-errors
6. .validation-summary-valid




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

فرض کنید مدل‌های برنامه شما به کمک یک code generator تولید می‌شوند. در این حالت هرگونه ویژگی اضافی تعریف شده در این کلاس‌ها پس از تولید مجدد کدها از دست خواهند رفت. به همین منظور امکان تعریف مجزای متادیتاها نیز پیش بینی شده است:

[MetadataType(typeof(CustomerMetadata))]
public partial class Customer
{
class CustomerMetadata
{

}
}

public partial class Customer : IValidatableObject
{


حالت کلی روش انجام آن هم به شکلی است که ملاحظه می‌کنید. کلاس اصلی، به صورت partial معرفی خواهد شد. سپس کلاس partial دیگری نیز به همین نام که در برگیرنده یک کلاس داخلی دیگر برای تعاریف متادیتا است، به پروژه اضافه می‌گردد. به کمک ویژگی MetadataType، کلاسی که قرار است ویژگی‌های خواص از آن خوانده شود، معرفی می‌گردد. موارد عنوان شده، شکل کلی این پیاده سازی است. برای نمونه اگر با WCF RIA Services کار کرده باشید، از این روش زیاد استفاده می‌شود. کلاس خصوصی تو در توی تعریف شده صرفا وظیفه ارائه متادیتاهای تعریف شده را به فریم ورک خواهد داشت و هیچ کاربرد دیگری ندارد.
در ادامه کلیه خواص کلاس Customer به همراه متادیتای آن‌ها باید به کلاس CustomerMetadata منتقل شوند. اکنون می‌توان تمام متادیتای کلاس اصلی Customer را حذف کرد.



اعتبار سنجی از راه دور (remote validation)

فرض کنید شخصی مشغول به پر کردن فرم ثبت نام، در سایت شما است. پس از اینکه نام کاربری دلخواه خود را وارد کرد و مثلا به فیلد ورود کلمه عبور رسید، در همین حال و بدون ارسال کل صفحه به سرور، به او پیغام دهیم که نام کاربری وارد شده، هم اکنون توسط شخص دیگری در حال استفاده است. این مکانیزم از ASP.NET MVC3 به بعد تحت عنوان Remote validation در دسترس است و یک درخواست Ajaxایی خودکار را به سرور ارسال خواهد کرد و نتیجه نهایی را به کاربر نمایش می‌دهد؛ کارهایی که به سادگی توسط کدهای جاوا اسکریپتی قابل مدیریت نیستند و نیاز به تعامل با سرور، در این بین وجود دارد. پیاده سازی آن هم به نحو زیر است:
برای مثال خاصیت Name را در مدل برنامه به نحو زیر تغییر دهید:

[Required(ErrorMessage = "Name is required.")]
[StringLength(50)]
[System.Web.Mvc.Remote(action: "CheckUserNameAndEmail",
controller: "Customer",
AdditionalFields = "Email",
HttpMethod = "POST",
ErrorMessage = "Username is not available.")]
public string Name { set; get; }

سپس متد زیر را نیز به کنترلر Customer اضافه کنید:

[HttpPost]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult CheckUserNameAndEmail(string name, string email)
{
if (name.ToLowerInvariant() == "vahid") return Json(false);
if (email.ToLowerInvariant() == "name@site.com") return Json(false);
//...
return Json(true);
}


توضیحات:
توسط ویژگی System.Web.Mvc.Remote، نام کنترلر و متدی که در آن قرار است به صورت خودکار توسط jQuery Ajax فراخوانی شود، مشخص خواهند شد. همچنین اگر نیاز بود فیلدهای دیگری نیز به این متد کنترلر ارسال شوند، می‌توان آن‌ها را توسط خاصیت AdditionalFields، مشخص کرد.
سپس در کدهای کنترلر مشخص شده، متدی با پارامترهای خاصیت مورد نظر و فیلدهای اضافی دیگر، تعریف می‌شود. در اینجا فرصت خواهیم داشت تا برای مثال پس از بررسی بانک اطلاعاتی، خروجی Json ایی را بازگردانیم. return Json false به معنای شکست اعتبار سنجی است.
توسط ویژگی OutputCache، از کش شدن نتیجه درخواست‌های Ajaxایی جلوگیری کرده‌ایم. همچنین نوع درخواست هم جهت امنیت بیشتر، به HttpPost محدود شده است.
تمام کاری که باید انجام شود همین مقدار است و مابقی مسایل مرتبط با اعمال و پیاده سازی آن خودکار است.


استفاده از مکانیزم اعتبار سنجی مبتنی برمتادیتا در خارج از ASP.Net MVC

مباحثی را که در این قسمت ملاحظه نمودید، منحصر به ASP.NET MVC نیستند. برای نمونه توسط متد الحاقی زیر نیز می‌توان یک مدل را مثلا در یک برنامه کنسول هم اعتبار سنجی کرد. بدیهی است در این حالت نیاز خواهد بود تا ارجاعی را به اسمبلی System.ComponentModel.DataAnnotations، به برنامه اضافه کنیم و تمام عملیات هم دستی است و فریم ورک ویژه‌ای هم وجود ندارد تا یک سری از کارها را به صورت خودکار انجام دهد.

using System.ComponentModel.DataAnnotations;

namespace MvcApplication9.Helper
{
public static class ValidationHelper
{
public static bool TryValidateObject(this object instance)
{
return Validator.TryValidateObject(instance, new ValidationContext(instance, null, null), null);
}
}
}



نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
آیا میشود در کنار سیستم احراز هویت DNT Identity سیستم احراز هویت Active Directory را پیاده سازی کردی؟
به اینصورت که در Request کاربر چک شود که اگر از کاربران جوین دامین است نیاز به لاگین نباشد و ساخت کوکی و لاگین پشت سیستم انجام شود و فقط دسترسی آن چک شود. درغیر اینصورت کاربر به صفحه لاگین راهنمایی شود. نیاز دارم که هر دو سیستم احراز هویت را داشته باشم...برای کاربران داخل و خارج سازمان 
مطالب
کار با دیتاتایپ JSON در MySQL - قسمت اول
تا قبل از اضافه شدن دیتاتایپ JSON به صورت توکار در MySQL، داده‌های JSON را تنها میتوانستیم با فرمت رشته‌ای، درون دیتابیس ذخیره کنیم: 
CREATE TABLE tableName (
jsonData CHAR(250) -- or VARCHAR, TEXT, BLOB
);

INSERT INTO tableName VALUES (
'{ "name": "User1", "age": 41}'
);

SELECT * FROM tableName;

{ "name": "User1", "age": 41}
اما مشکل اینجاست که هیچ نوع اعتبارسنجی بر روی این دیتا صورت نخواهد گرفت؛ هیچ روشی برای مطمئن شدن از اینکه تگ‌ها به درستی استفاده شده‌اند، وجود ندارد و همچنین امکان جستجو را سخت خواهد کرد؛ زیرا مجبور خواهیم بود از Regular Expressions برای جستجوی درون متن‌های ذخیره شده استفاده کنیم:
SELECT * FROM tableName
WHERE jsonData REGEXP 'User1';

از نسخه MySQL 5.7.8 به بعد، می‌توانیم از نوع داده JSON برای ذخیره‌سازی محتوای JSON، استفاده کنیم. از این دیتاتایپ برای ذخیره‌سازی یک JSON document معتبر میتوان استفاده کرد:
CREATE TABLE tableName (
jsonData JSON
);

INSERT INTO tableName VALUES (
'{ "name": "User1", "age": 41, "name": "User2"}'
);

SELECT * FROM tableName;

{"age": 41, "name": "User2"}

همانطور که مشاهده میکنید MySQL به صورت اتوماتیک یکسری نرمال‌سازی را روی دیتا اعمال کرده است:
  • ابتدا بررسی خواهد شد که سند JSON معتبر باشد؛ در غیر اینصورت ذخیره‌سازی با مشکل مواجه خواهد شد.
  • از فیلدهایی که کلید تکراری دارند، صرفنظر خواهند شد. در مثال بالا دوبار فیلد name را مقداردهی کرده‌ایم. در اینجالت key/value دوم لحاظ شده‌است. البته میبایستی اصل first key wins لحاظ میشد، اما این مورد به عنوان یک باگ گزارش شده‌است و در نسخه‌های 8 به بعد رفع شده‌است (https://forums.mysql.com/read.php?3,660500,660500 - https://bugs.mysql.com/bug.php?id=86866).
  • فاصله‌های اضافی بین کلیدها حذف شده‌اند.
  • برای جستجوی بهتر، کلیدهای آبجکت JSON به صورت مرتب شده ذخیره شده‌اند.

جستجو درون JSON Document
یک سند JSON، از یکسری کلیدها به همراه مقادیرشان تشکیل شده‌است. همچنین مقادیر میتوانند شامل اشیاء یا آرایه‌هایی به صورت تودرتو باشند. بنابراین به یک path جهت استخراج مقادیر نیاز خواهیم داشت. برای نوشتن یک path باید scope آن را تعیین کنیم که در توابع MySQL این scope به صورت پیش‌فرض، سند جاری میباشد که توسط علامت $ مشخص میشود. 
فرض کنید ساختار زیر را درون دیتابیس ذخیره کرده‌ایم:
{
    "id": "1",
    "sku": "asdf123",
    "name": "Lorem ipsum jacket",
    "price": 12.45,
    "discount": 10,
    "offerEnd": "October 5, 2020 12:11:00",
    "new": false,
    "rating": 4,
    "saleCount": 54,
    "category": ["fashion", "men"],
    "tag": ["fashion", "men", "jacket", "full sleeve"],
    "variation": [
      {
        "color": "white",
        "image": "/assets/img/product/fashion/1.jpg",
        "size": [
          {
            "name": "x",
            "stock": 3
          },
          {
            "name": "m",
            "stock": 2
          },
          {
            "name": "xl",
            "stock": 5
          }
        ]
      },
      {
        "color": "black",
        "image": "/assets/img/product/fashion/8.jpg",
        "size": [
          {
            "name": "x",
            "stock": 4
          },
          {
            "name": "m",
            "stock": 7
          },
          {
            "name": "xl",
            "stock": 9
          },
          {
            "name": "xxl",
            "stock": 1
          }
        ]
      },
      {
        "color": "brown",
        "image": "/assets/img/product/fashion/3.jpg",
        "size": [
          {
            "name": "x",
            "stock": 1
          },
          {
            "name": "m",
            "stock": 2
          },
          {
            "name": "xl",
            "stock": 4
          },
          {
            "name": "xxl",
            "stock": 0
          }
        ]
      }
    ],
    "image": [
      "/assets/img/product/fashion/1.jpg",
      "/assets/img/product/fashion/3.jpg",
      "/assets/img/product/fashion/6.jpg",
      "/assets/img/product/fashion/8.jpg",
      "/assets/img/product/fashion/9.jpg"
    ],
    "description": {
      "shortDescription": "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur.",
      "fullDescription": "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?"
    }
  }

برای دریافت دسته‌بندی‌های هر ردیف میتوانیم از تابع JSON_EXTRACT استفاده کنیم:
SELECT 
    JSON_PRETTY(
  JSON_EXTRACT(data, "$.category")
    )
FROM
    experiments.productMetadata;

/* 
  [
    "fashion",
    "men"
  ]
  [
    "fashion",
    "women"
  ]
  [
    "fashion",
    "men"
  ]
*/
همانطور که مشاهده میکنید، تابع JSON_EXTRACT یک آرگومان دومی را نیز دریافت میکند که توسط آن میتوانیم path موردنظر را وارد کنیم و همانطور که عنوان شد، از $ برای دسترسی به سند جاری استفاده میکنیم. سپس در ادامه نام پراپرتی‌ای را که میخواهیم استخراج کنیم، تعیین کرده‌ایم. در اینجا چون ساختار ذخیره شده، به صورت شیء میباشد، به صورت مستقیم از $ و بعد از آن نقطه و سپس نام پراپرتی استفاده کرده‌ایم. میتوانیم عمق پیمایش را نیز بیشتر کنیم. به عنوان مثال برای دسترسی به المنت دوم از آرایه tag درون دیتا خواهیم داشت:
JSON_EXTRACT(data, "$.tag[1]")
JSON_EXTRACT(data, "$.description.shortDescription")

همچنین اگر کلید مقداری را که میخواهیم جستجو کنیم، بدانیم اما از کلید والد آن اطلاع نداشته باشیم، میتوانیم از * استفاده کنیم: 
SELECT 
JSON_EXTRACT(data, "$.*.shortDescription")
FROM experiments.productMetadata;

JSON_KEYS
از این تابع جهت دریافت کلیدهای top level یک شیء JSON استفاده میشود:
SELECT 
JSON_KEYS(data)
FROM experiments.productMetadata;

-- ["id", "new", "sku", "tag", "name", "image", "price", "rating", "category", "discount", "offerEnd", "saleCount", "variation", "description"]
-- ["id", "new", "sku", "tag", "name", "image", "price", "rating", "category", "discount", "saleCount", "variation", "description"]


همچنین میتوانیم path را نیز به عنوان آرگومان دوم آن تعیین کنیم: 
SELECT 
JSON_KEYS(data, "$.description")
FROM experiments.productMetadata;

-- ["fullDescription", "shortDescription"]
-- ["fullDescription", "shortDescription"]

JSON_CONTAINS 
از این تابع برای جستجو استفاده خواهیم کرد و همانطور که از نام آن پیداست، در صورت وجود مقدار مورد جستجو، خروجی ۱ خواهد بود:
SELECT 
    JSON_CONTAINS(data, "10", "$.discount")
FROM
    experiments.productMetadata;

-- 1
-- 0

JSON_CONTAINS_PATH
توسط این تابع میتوانیم بررسی کنیم که یک path یا یک یکسری path خاص درون JSON document وجود دارند یا خیر: 
SELECT 
JSON_CONTAINS_PATH(data, "one", "$.description", "$.address", "$.website")
FROM experiments.productMetadata;
آرگومان اول این تابع، داکیومنتی است که میخواهیم جستجو کنیم. برای آرگومان دوم، یکی از دو مقدار one یا all را میتوانیم تنظیم کنیم. در ادامه لیستی از pathهایی را که میخواهیم جستجو کنیم، وارد کرده‌ایم. در حالت one، اگر تنها یکی از pathها درون داکیومنت JSON وجود داشته باشند، خروجی ۱ خواهد بود. اگر one را به all تنظیم کنیم، یعنی باید تمامی pathها، درون داکیومنت وجود داشته باشند تا خروجی ۱ شود؛ در غیراینصورت خروجی ۰ خواهد بود. 

JSON_SEARCH  
توسط این تابع میتوانیم position مقدار مورد جستجو را درون داکیومنت JSON پیدا کنیم: 
SELECT 
    JSON_SEARCH(data, 'one', 'fashion')
FROM
    experiments.productMetadata;
    
-- "$.tag[0]"
-- "$.tag[0]"

مطالب
Cookie - قسمت دوم

کوکی در جاوا اسکریپت 

همانطور که در قسمت قبل اشاره کوتاهی شد، مدیریت کوکی‌های در دسترس در وضعیت جاری، در جاوا اسکریپت ازطریق پراپرتی cookie از شی document امکان‌پذیر است. این پراپرتی کاری همانند هدرهای Set-Cookie و Cookie (که در قسمت قبل درباره آن‌ها بحث شد) انجام می‌دهد. این پراپرتی یک مورد کاملا استثنایی و نسبتا عجیب در زبان جاوا اسکریپت است. در نگاه اول ظاهرا document.cookie از نوع رشته است، اما قضیه کاملا فرق می‌کند. برای روشن شدن مطلب به ادامه بحث توجه کنید.

افزودن کوکی
- برای افزودن یا ویرایش یک کوکی باید از ساختاری مانند ساختار هدر Set-Cookie که چیزی شبیه به عبارت زیر است، پیروی کرد:
document.cookie = "name=value; expires=date; domain=theDomain; path=thePath; secure";
 
نکته: با توجه به توضیحاتی که در قسمت قبل ارائه شد، بدیهی است که امکان ثبت یک کوکی با فلگ HttpOnly در جاوا اسکریپت وجود ندارد!
 
اجرای دستوری شبیه با ساختار نشان داده شده در بالا، موجب حذف کوکی‌های قبلی نمی‌شود. از این دستور برای ایجاد یک کوکی و یا ویرایش یک کوکی موجود استفاده می‌شود. کوکی‌های ایجادشده با این روش تفاوتی با کوکی‌های ایجادشده توسط هدر Set-Cookie ندارند و همانند آنها در درخواست‌های بعدی با توجه به خواص تنظیم شده، به سمت سرور ارسال خواهند شد.
همانطور که مشاهده می‌کنید خاصیت‌های کوکی به صورت جفت‌های نام-مقدار درون یک رشته به document.cookie نسبت داده می‌شوند. این خاصیت‌ها توسط یک کاراکتر ; از یکدیگر جدا می‌شوند. شرح ساختار فوق در  زیر آورده شده است:
1. همیشه اولین جفتِ نام-مقدار همانند مثال بالا باید «عنوان و مقدار» کوکی را مشخص سازد. این قسمت تنها عضو اجباری ساختار فوق است.
2. سپس یک سمی‌کالن و یک فاصله
3. تاریخ انقضا (expires) یا حداکثر طول عمر کوکی (max-age)
4. سپس یک سمی‌کالن و یک فاصله
5. دمین و یا مسیر مربوط به کوکی
6. سپس یک سمی‌کالن و یک فاصله
7. سایر خواص چون Secure
نکته: این ساختار عجیب معرفی شده را عینا رعایت کنید. بقیه کار توسط مرورگر انجام خواهد شد.
نکته: قسمت‌های مختلف این ساختار case-sensitive نیست، البته به‌جز نام کوکی که کاملا case-sensitive است.
مثلا برای ثبت یک کوکی با عنوان myCookie و مقدار myValue و دمین d.com و مسیر test و طول عمر 5 روزه باید از دستور زیر استفاده کرد:
document.cookie = 'myCookie=myValue; max-age=432000; domain=d.com; path=/test';
 
خواندن کوکی
- برای خواندن کوکی‌ها تنها کافی است مقدار پراپرتی document.cookie بررسی شود. با اینکه از دستور نشان داده شده در بالا اینگونه برمی آید که پراپرتی document.cookie به رشته معرفی شده مقداردهی شده است، اما به محض خواندن این پراپرتی چیزی شبیه به عبارت زیر برگردانده میشود:
myCookie=myValue 
از بقیه خواص اثری نیست! این رفتار به دلیل حفط امنیت کوکی‌ها در تمام مرورگرها رعایت می‌شود.
- برای ثبت کوکی دیگری در وضعیت جاری کافی است یکبار دیگر دستور بالا را برای کوکی جدید به کار ببریم. مثلا به صورت زیر:
document.cookie = 'mySecondCookie=mySecondValue; path=/'
اینار یک کوکی سشنی بدون دمین و با مقدار / برای مسیر کوکی ثبت می‌شود! در این حالت کوکی قبلی دوباره نویسی و یا حذف نمی‌شود و تنها یک کوکی جدید به لیست کوکیهای مرورگر اضافه می‌شود! این رفتار عجیب از ویژگی‌های جالب document.cookie است.
- اگر مقدار document.cookie در این حالت خوانده شود مقدار زیر برگشت داده می‌شود:
myCookie=myValue; mySecondCookie=mySecondValue
باز هم خبری از سایر خاصیت‌ها نیست. ولی همانطور که می‌بینید کوکی دوم به لیست کوکی‌های مرورگر اضافه شده است.

نکته: عبارت برگشت داده شده از پراپرتی document.cookie همانند مقداری است که در هدر Cookie هر درخواست توسط مرورگر گنجانده می‌شود، یعنی جفت نام-مقدار کوکی‌ها به همراه یک ; و یک فاصله بین مقادیر هر کوکی. بنابراین برای بدست آوردن مقدار یک کوکی یکسری عملیات جهت Parse کردن داده‌های آن نیاز است!

متدها
امروزه کتابخانه‌های متعددی با استفاده از زبان جاوا اسکریپت برای برنامه نویسی سمت کلاینت وجود دارد که بیشتر آنها قابلیت‌هایی برای کار با کوکی‌ها نیز دارند. ازجمله می‌توان به jQuery و YUI اشاره کرد. پلاگین مخصوص کوکی‌ها در jquery در اینجا بحث شده است. برای کسب اطلاعات بیشتر درباره قابلیت‌های کار با کوکی در YUI نیز به اینجا مراجعه کنید. مطالب زیر صرفا برای روشن شدن بحث ارائه می‌شوند. بدیهی است که برای کارهای عملی بهتر است از کتابخانه‌های موجود استفاده شود.
با توجه به اطلاعات بالا از متدهای زیر می‌توان برای خواندن، افزودن و حذف کوکی‌ها استفاده کرد.

نکته: متدهای زیر از ترکیب چندین ریفرنس مختلف بدست آمده است. هرچند برای موارد خاص‌تر می‌توانند بیشتر سفارشی شوند.

افزودن و یا ویرایش کوکی
function setCookie(data, value) {
  if (typeof data === "string") {
    data = { name: data, value: value };
  };
  if (!data.name) throw "Cookie's name can not be null.";

  var cookie = escape(data.name) + "=" + escape(data.value);

  var expDate = null;
  if (data.expDays) {
    expDate = new Date();
    expDate.setDate(expDate.getDate() + data.expDays);
  }
  else if (data.expYear && data.expMonth && data.expDay) {
    expDate = new Date(data.expYear, data.expMonth, data.expDay);
  }
  else if (data.expires) {
    expDate = data.expires;
  }
  else if (data.maxAge) {
    expDate = new Date();
    expDate.setSeconds(expDate.getSeconds() + data.maxAge);
  }
  if (expDate != null) cookie += "; expires=" + expDate.toGMTString();

  if (data.domain)
    cookie += "; domain=" + escape(data.domain);

  if (data.path)
    cookie += "; path=" + escape(data.path);

  if (data.secure)
    cookie += "; secure";

  document.cookie = cookie;
  return document.cookie;
}
در کد فوق برای انکد کردن رشته‌های مورد استفاده از متد escape استفاده شده است. برای آشنایی با این متد به اینجا مراجعه کنید.
هم‌چنین کار کردن با نوع داده تاریخ در جاوا اسکریپت کمی متفاوت است. بنابراین برای آشنایی بیشتر با این نوع داده به اینجا رجوع کنید.
 
نکته: در متد بالا بدلیل عدم پشتیبانی از خاصیت max-age در نسخه‌های قدیمی اینترنت اکسپلورر (نسخه 8 و قبل از آن) تنها از خاصیت expires استفاده شده است.
 
نحوه استفاده از متد بالا به صورت زیر است:
setCookie('cookie1', 'Value1');
setCookie({name:'cookie1', value:'Value1'});
setCookie({name:'cookie2', value:'Value2', expDays:10});
setCookie({name:'cookie3', value:'Value3', expires:new Date()});
setCookie({name:'cookie4', value:'Value4', expYear:2013, expMonth:0, expDay:13});
setCookie({name:'cookie3', value:'Value3', maxAge:365*24*60*60});
setCookie({name:'cookie5', value:'Value5', domain:'d.net', path:'/'});
setCookie({name:'cookie6', value:'Value6', secure:true});
setCookie({name:'cookie7', value:'Value7', expDays:100, domain:'dd.com', path:'/employee', secure:true});
 
حذف کوکی
همانطور که در قسمت قبل هم اشاره شد، برای حذف یک کوکی، کافی است تا تاریخ انقضای آن به تاریخی در گذشته مقداردهی شود. بنابراین برای اینکار می‌توان از متد زیر استفاده کرد:
function delCookie(data) {
  if (typeof data === "string") {
    data = { name: data };
  };
  data.expDays = -1;
  return setCookie(data);
}
در متد فوق از متد setCookie که در بالا معرفی شد، استفاده شده است. نحوه استفاده از این متد هم به صورت زیر است:
delCookie('myCookie');
delCookie({ name: 'myCookie', domain: 'd.com', path: '/test' });
 
خواندن کوکی
برای خواندن مقدار یک کوکی می‌توان از متد زیر استفاده کرد:
function getCookie(name) {
  var cookies = document.cookie.split(";");
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].split("=");
    if (cookie[0].trim() == escape(name)) {
      return unescape(cookie[1].trim());
    }
  }
  return null;
}
برای آشنایی با متد unescape که در بالا از آن استفاده شده است به اینجا مراجعه کنید. در متد فوق از متد trim زیر استفاده شده است:
String.prototype.trim = function () {
  return this.replace(/^\s+|\s+$/g, "");
};
String.prototype.trimStart = function () {
  return this.replace(/^\s+/, "");
};
String.prototype.trimEnd = function () {
  return this.replace(/\s+$/, "");
};
این متدها از اینجا گرفته شده است.
روش استفاده شده برای خواندن مقادیر کوکی‌ها در متد بالا بسیار ساده و ابتدایی است و صرفا برای آشنایی با نحوه Parse کردن رشته برگشت داده شده توسط document.cookie ارائه شده است. روش‌های مناسب‌تر و مطمئن‌تر با یک جستجوی ساده در دسترس هستند. البته همانطور که قبلا هم اشاره شد، استفاه از کتابخانه‌های موجود راه‌حل بهتری است.
هم‌چنین ازآنجاکه مقدار یک کوکی می‌تواند شامل کاراکتر = نیز باشد، بنابراین قسمت return متد فوق را می‌توان به صورت زیر تغییر داد:
cookie.shift(1);
return unescape(cookie.join('=').trim());
 
نکته: با توجه به مطالب ارائه شده در قسمت قبل  بدست آوردن مقادیر کوکی‌ها کمی پیچیده‌تر از دیگر عملیات‌هاست. ازآنجاکه راه مستقیمی با استفاده از جاوا اسکریپت برای یافتن سایر خواص کوکی وجود ندارد، بنابراین بدست آوردن مقدار دقیق کوکی موردنظر ممکن است غیرممکن باشد! (با توجه به اینکه کوکی‌های متفاوت می‌توانند نام‌های یکسانی داشته باشند).
 
با توجه به نکته بالا، حال اگر با یک نام بخصوص، چندین کوکی ثبت شده باشد (با خواص متفاوت)، یکی از راه‌حل‌ها این است که آرایه‌ای از مقادیر این کوکی‌های همنام برگشت داده شود. بنابراین متد فوق را می‌توان به صورت زیر تکمیل کرد:
function getCookie(name) {
  var foundCookies = [];
  var cookies = document.cookie.split(";");
  for (var i = 0; i < cookies.length; i++) {
    var cookie = cookies[i].split("=");
    if (cookie[0].trim() == escape(name) && cookie.length >= 2) {
      cookie.shift(1);
      foundCookies.push(unescape(cookie.join('=').trim()));
    }
  }
  return foundCookies.length > 1 
            ? foundCookies
            : foundCookies.length == 1
                ? foundCookies[0]
                : null;
}

خلاصه‌ای از نحوه استفاده از متدهای بالا در IE8 (برای نمایش اجرای درست در مرورگری قدیمی!) در تصویر زیر  نشان داده شده است:

 
نکته: کار توسعه این متدها را میتوان برای پشتیبانی از SubCookieها نیز ادامه داد، اما به دلیل دورشدن از مبحث اصلی، این موضوع در این مطلب ارائه نمیشود (درباره این نوع از کوکی‌ها در قسمت قبل شرح کوتاهی داده شده است). اگر علاقه‌مند و نیازمند به این نوع کوکی‌ها هستید، کتابخانه YUI پشتیبانی کاملی از آنها ارائه میکند.
 
در قسمت بعدی به نکات کار با کوکی در ASP.NET میپردازیم.

منابع:
مطالب
نمایش خطاهای اعتبارسنجی سمت کاربر ASP.NET MVC به شکل Popover به کمک Twitter bootstrap
این مطلب در ادامه بحث «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» می‌باشد. بنابراین تعاریف مدل و کنترلر آن، به همراه توضیحات ذکر شده در آن، در ادامه مورد استفاده قرار خواهند گرفت.


اصول نمایش Popover در Twitter bootstrap

PopOverها نیز یکی دیگر از کامپوننت‌های جاوا اسکریپتی مجموعه بوت استرپ هستند. بسیار شبیه به Tooltip بوده، اما ماندگارتر هستند. PopOverها با کلیک بر روی یک عنصر باز شده و تنها با کلیک مجدد بر روی آن المان، بسته می‌شوند (البته این موارد نیز قابل تنظیم هستند).
<a rel="popover" 
               data-content="محتوایی برای نمایش" 
               data-original-title="عنوان" href="#">اطلاعات</a>

    <script type="text/javascript">
        $(document).ready(function () {
            $("[rel='popover']").popover({ placement: 'left' })
                            .click(function (e) { e.preventDefault(); });
        });
   </script>
نحوه استفاده از آن را در مثال فوق مشاهده می‌کنید. در اینجا یک لینک با rel=popover تعریف شده است. از این rel، در یافتن کلیه المان‌هایی اینگونه، توسط jQuery استفاده خواهیم کرد. سپس مقدار ویژگی data-content، محتوای اطلاعاتی را که باید نمایش داده شود، مشخص می‌کند. همچنین برای مشخص ساختن عنوان آن می‌توان از ویژگی data-original-title استفاده کرد. نهایتا نیاز است افزونه popover بر روی المان‌هایی با rel=popover فراخوانی گردد. در روال رخدادگردان click آن، با استفاده از e.preventDefault، سبب خواهیم شد تا با کلیک بر روی لینک تعریف شده، صفحه مجددا بازیابی نشده و مکان اسکرول عمودی صفحه، تغییر نکند.


تبدیل خطاهای اعتبارسنجی ASP.NET MVC به PopOver

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

همانطور که ملاحظه می‌کنید، اینبار بجای نمایش خطاها در یک برچسب، مقابل کنترل متناظر، این خطا صرفا در حالت فوکوس کنترل، به شکل یک PopOver در کنار آن ظاهر شده است.


کدهای کامل View برنامه

@model Mvc4TwitterBootStrapTest.Models.User
@{
    ViewBag.Title = "Index";
}
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true, null, new { @class = "alert alert-error alert-block" })

    <fieldset class="form-horizontal">
        <legend>تعریف کاربر جدید</legend>
        <div class="control-group">
            @Html.LabelFor(model => model.Name, new { @class = "control-label" })
            <div class="controls">
                @Html.EditorFor(model => model.Name)
                @*@Html.ValidationMessageFor(model => model.Name, null, new { @class = "help-inline" })*@
            </div>
        </div>
        <div class="control-group">
            @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
            <div class="controls">
                @Html.EditorFor(model => model.LastName)
                @*@Html.ValidationMessageFor(model => model.LastName, null, new { @class = "help-inline" })*@
            </div>
        </div>
        <div class="form-actions">
            <button type="submit" class="btn btn-primary">
                ارسال</button>
        </div>
    </fieldset>
}
@section JavaScript
{
    <script type="text/javascript">
        $.validator.setDefaults({
            showErrors: function (errorMap, errorList) {
                this.defaultShowErrors();
                //اگر المانی معتبر است نیاز به نمایش پاپ اور ندارد
                $("." + this.settings.validClass).popover("destroy");
                //افزودن پاپ اورها
                for (var i = 0; i < errorList.length; i++) {
                    var error = errorList[i];
                    $(error.element).popover({ placement: 'left' })
                                    .attr("data-original-title", "خطای اعتبارسنجی")
                                    .attr("data-content", error.message);
                }
            },
            // همانند قبل برای رنگی کردن کل ردیف در صورت عدم اعتبار سنجی و برعکس
            highlight: function (element, errorClass, validClass) {
                if (element.type === 'radio') {
                    this.findByName(element.name).addClass(errorClass).removeClass(validClass);
                } else {
                    $(element).addClass(errorClass).removeClass(validClass);
                    $(element).closest('.control-group').removeClass('success').addClass('error');
                }
                $(element).trigger('highlited');
            },
            unhighlight: function (element, errorClass, validClass) {
                if (element.type === 'radio') {
                    this.findByName(element.name).removeClass(errorClass).addClass(validClass);
                } else {
                    $(element).removeClass(errorClass).addClass(validClass);
                    $(element).closest('.control-group').removeClass('error').addClass('success');
                }
                $(element).trigger('unhighlited');
            }
        });
        //برای حالت پست بک از سرور عمل می‌کند
        $(function () {
            $('form').each(function () {
                $(this).find('div.control-group').each(function () {
                    if ($(this).find('span.field-validation-error').length > 0) {
                        $(this).addClass('error');
                    }
                });
            });
        });
   </script>
}
کدهای مدل و کنترلر، همانند مطلب «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» می‌باشند و از تکرار مجدد آن‌ها در اینجا صرفنظر گردید.

توضیحات
- با توجه به اینکه دیگر نمی‌خواهیم خطاها به صورت برچسب در مقابل کنترل‌ها نمایش داده شوند، کلیه Html.ValidationMessageFor به صورت کامنت درآورده شده‌اند.
- تغییر دوم مطلب جاری، اضافه شدن متد showErrors به تنظیمات پیش فرض jQuery Validator است. در این متد، اگر المانی معتبر بود، Popover آن حذف می‌شود یا در سایر حالات، المان‌هایی که نیاز به اعتبارسنجی سمت کلاینت دارند، یافت شده و سپس ویژگی data-content با مقداری معادل خطای اعتبارسنجی متناظر، به این المان افزوده و سپس متد popover بوت استرپ بر روی آن فراخوانی می‌گردد.
به عبارتی زمانیکه یک input box در ASP.NET MVC به همراه مقادیر مرتبط با اعتبارسنجی آن رندر می‌شود، چنین شکلی را خواهد داشت:
<input class="text-box single-line" data-val="true" data-val-required="لطفا نام را تکمیل کنید"
 id="Name" name="Name" type="text" value="" />
اما در اینجا به صورت پویا، data-original-title و data-content نیز به آن افزوده می‌گردند:
<input class="text-box single-line input-validation-error" data-val="true" data-val-required="لطفا نام را تکمیل کنید"
 id="Name" name="Name" type="text" value="" 
data-original-title="خطای اعتبارسنجی" title="" data-content="لطفا نام را تکمیل کنید">
این مقادیر توسط افزونه popover بوت استرپ شناسایی شده و مورد استفاده قرار می‌گیرد.
البته این موارد را در صورت نیاز به صورت دستی نیز می‌توان تعریف و اضافه کرد:
 @Html.TextBoxFor(x => x.Name, 
         new { data_content = "Name is required", 
               data_original_title = "Error", rel="popover" })
مطالب
آموزش Knockout.Js #4
مقید سازی رویداد کلیک
Click Binding روشی است برای اضافه کردن یک گرداننده رویداد در زمانی که قصد داریم یک تابع جاوااسکریپتی را در هنگام کلیک بر روی المان مورد نظر فراخوانی کنیم. از این مقید سازی عموما در عناصر button و input و تگ a استفاده می‌شود. اما در حقیقت در تمام عناصر غیر پنهان صفحه مورد استفاده قرار می‌گیرد.
<div>
    Number Of Clicks <span data-bind="text: numberOfClicks"></span> times
    <button data-bind="click: clickMe">Click me</button>
</div>
 
<script type="text/javascript">
    var viewModel = {
        numberOfClicks : ko.observable(0),
        clickMe: function() {
            var previousCount = this.numberOfClicks();
            this.numberOfClicks(previousCount + 1);
        }
    };
</script>
رویداد کلیک  button در کد بالا به تابعی با نام clickMe مقید شده است. این تابع در viewModel جاری صفحه تعریف شده است و در بدنه آن تعداد کلیک‌های قبلی را به علاوه یک خواهد کرد. از آنجا که تگ span در بالای صفحه به تعداد کلیک‌ها مقید شده است در نتیجه همواره مقدار این تگ به روز خواهد بود.

*نکته اول: اگر قصد داشته باشیم که عنصر جاری در viewModel را به گرداننده رویداد پاس دهیم چه باید کرد؟
هنگام فراخوانی رویدادها، KO به صورت پیش فرض مقدار جاری مدل را به عنوان اولین پارامتر به این گرداننده پاس می‌دهد. این روش مخصوصا در هنگامی که قصد اجرای عملیاتی خاص بر روی تک تک عناصر یک مجموعه را داشته باشید(مثل حلقه foreach) بسیار مفید خواهد بود.
<ul data-bind="foreach: places">
    <li>
        <span data-bind="text: $data"></span>
        <button data-bind="click: $parent.removePlace">Remove</button>
    </li>
</ul>
 
 <script type="text/javascript">
     function MyViewModel() {
         var self = this;
         self.places = ko.observableArray(['Tehran', 'Esfahan', 'Shiraz']);
 
         self.removePlace = function(place) {
             self.places.remove(place)
         }
     }
     ko.applyBindings(new MyViewModel());
</script>
در تابع removePlace می‌بینید که مقدار آیتم جاری در لیست به عنوان اولین آرگومان به این تابع پاس داده می‌شود، در نتیجه می‌دانیم که کدام عنصر را باید از لیست مورد نظر حذف کنیم. برای به دست آوردن آیتم جاری در لیست از parent$ یا root$ می‌توان استفاده کرد.
همان طور که پست قبل توضیح داده شد؛ برای اینکه بتوانیم از یک viewModel به مجموعه از عناصر در  یک حلقه foreach مقید کنیم امکان استفاده از اشاره گر this میسر نیست. در نتیجه بهتر است در ابتدای viewModel مقدار این اشاره گر را در یک متغیر معمولی (در اینجا به نام self است) ذخیره کنیم و از این پس این متغیر را برای اشاره به عناصر viewModel به کار بریم. در اینجا self به عنواتن یک alias برای this خواهد بود.

*نکته دوم: دسترسی به عنصر رویداد
در بعضی مواقع نیاز است در حین فراخوانی رویداد ،عنصر رویداد DOM  به عنوان فرستنده در اختیار تابع گرداننده قرار گیرد. خبر خوش این است که KO به صورت پیش فرض این عنصر را نیز به عنوان پارامتر دوم به توابع گرداننده رویداد پاس می‌دهد. برای مثال:
<button data-bind="click: myFunction">
    Click me
</button>
 
 <script type="text/javascript">
    var viewModel = {
        myFunction: function(data, event) {
            if (event.shiftKey) {
               
            } else {               
            }
        }
    };
    ko.applyBindings(viewModel);
</script>
تابع myFunction در مثال بالا دارای دو پارامتر است. پارامتر دوم در این تابع به عنوان عنصر فرستنده رویداد مورد استفاده قرار خواهد گرفت. بدین ترتیب در توابع event Handler‌ها می‌توان به راحتی اطلاعات مورد نیاز درباره آبجکت رویداد را به دست آورد.

*نکته سوم: به صورت پیش فرض KO از اجرای عملیات پیش فرض رویداد‌ها جلوگیری به عمل می‌آورد. این به این معنی است که اگر برای رویداد کلیک تگ a بک تابع گرداننده تعریف کرده باشید، بعد از کلیک بر روی این المان؛ مرورگر فقط این تابع تعریف شده توسط شما را فراخوانی خواهد کرد و دیگر عملیات راهبری به صفحه مورد نظر در خاصیت href صورت نخواهد گرفت. اگر به هر دلیلی قصد داشته باشیم که این رفتار صورت نگیرد کافیست در انتهای تابع گرداننده رویداد مقدار true برگشت داده شود.

*نکته چهارم: مفهوم clickBubble
ابتدا به کد زیر دقت کنید:
<div data-bind="click: myDivHandler">
    <button data-bind="click: myButtonHandler">
        Click me
    </button>
</div>
همان طور که مشاهده می‌کنید در کد بالا برای عنصر button یک رویداد کلید تعریف شده است. از طرف دیگر این button درون تگ div قرار دارد که برای این تگ نیز این رویداد کلیک با تابع گرداننده متفاوتی تعریف شده است. نکته این جاست که به صورت پیش فرض بعد از فراخوانی رویداد کلیک عنصر داخلی، رویداد کلیک عنصر خارجی نیز فراخوانی خواهد شد. به این رفتار event bubbling می‌گویند. اگر قصد داشته باشیم که این رفتار را غیر فعال کنیم(بعنی با کلیک بر روی button، رویداد کلیک تگ div اجرا نشود باید مقدار خاصبت clickBubble رویداد عنصر داخلی را برابر false قرار دهیم) به صورت زیر:
<div data-bind="click: myDivHandler">
    <button data-bind="click: myButtonHandler, clickBubble: false">
        Click me
    </button>
</div>