مطالب
MTOM در WCF
در  WCF سه نوع Message Encoder وجود دارد:
  • Text(Xml) Message Encoder(به صورت پیش فرض در تمام Http-Base Binding‌ها از این Encoder استفاده می‌شود)
  • Binary Message Encoder(به صورت پیش فرض در تمام Net* Binding‌ها از این encoder استفاده می‌شود که برای سرویس‌های وب مناسب نیست)
  • MTOM Message Encoder (در حالت استفاده از Http-Base Binding‌ها و انتقال اطلاعات به صورت باینری از این گزینه استفاده می‌شود که به صورت پیش فرض غیر فعال است)
Encoding یا رمزگذاری در WCF به این معنی است که داده‌های مورد نظر برای انتقال، به یکی از فرمت‌های MTOM ، Text-Xml یا  Binary سریالایز شوند.
 وضعیتی را در نظر بگیرید که در یک پروژه مبتنی بر WCF قصد دارید حجمی زیاد از داده به فرمت باینری  (نظیر فایل ها) را بین سرور و کلاینت رد و بدل کنید. به صورت معمول بسیاری از برنامه نویسان، یک کلاس به همراه DataContractAttribute ایجاد می‌کنند که در آن خاصیتی به صورت آرایه ای از نوع بایت که DataMemberAttribute را نیز دارد برای انتقال محتویات فایل استفاده می‌شود. اما باید یک نکته را مد نظر داشت و آن این است که به صورت پیش فرض فرمت انتقال داده‌ها در WCF به صورت Text/Xml است  و برای انتقال داده‌ها نیز از فرمت Base 64 استفاده خواهد شد. مشکل اصلی این جاست که در حالت Text/Xml Encoding برای انتقال داده‌های باینری، برای هر سه بایت، چهار کاراکتر استفاده می‌شود در نتیجه، این باعث افزایش حجم داده تا 33 درصد خواهد شد که کارایی سیستم را تحت تاثیر قرار می‌دهد.
اما خبر خوب این است که استانداردی وجود دارد به نام MTOM یا همان Message Transmission Optimization Mechanism، برای این که بتوان محتوای باینری را بدون افزایش حجم داده انتقال داد. برای پیاده سازی این روش باید موارد زیر را در نظر داشته باشید:

»متد یا همان OperationContract که وظیفه آن ارسال یا دریافت داده‌ها با فرمت MTOM است فقط کلاس هایی را انتقال دهد که دارای MessageContractAttribute هستند. نباید از DataContractAttribute استفاده نمایید.
»خاصیتی که نوع آن آرایه ای از بایت‌ها است نباید دارای DataMemberAttribute باشد؛ بلکه به جای آن باید از MessageBodyMember استفاده نمایید.
»به جای []Byte می‌توان از نوع Stream نیز استفاده کرد(الزامی نیست).
»مقدار خاصیت MessageEncoding در Binding استفاده شده باید MTOM تعیین شود.
پیاده سازی یک مثال
ابتدا کلاس مورد نظر را به صورت زیر تهیه می‌کنیم:
[MessageContract]
public class MyFile
{
    [MessageHeader] 
    public String Filename { get; set; }

    [MessageBodyMember]    
    public Byte[] Contents { get; set; } 
}
چند تذکر
  • به جای DataContract از MessageContract استفاده می‌شود؛
  • تمام خاصیت هایی که نوع آن‌ها غیر از []Byte است باید دارای MessageHeader باشند؛
  • خاصیتی که برای انتقال محتوای باینری تهیه شده است، باید از MessageBodyMember استفاده نماید؛
  • مجاز به تعریف فیلد یا فیلد هایی که نوع آن‌ها Primitive Type است نمی‌باشید.

تنظیمات مربوط به Binding نیز به صورت خواهد بود:

<bindings> 
    <wsHttpBinding>
        <binding name="WsHttpMtomBinding" messageEncoding="Mtom" /> 
    </wsHttpBinding>
 </bindings>
اما یک نکته...
هدف از استفاده از MTOM برای افزایش کارایی انتقال داده‌های باینری در حجم زیاد است. در زیر نتایج مقایسه بررسی انتقال اطلاعات به دو صورت MTOM و Text برای حجم داده‌های متفاوت را مشاهده می‌کنید:


با دقت در نتایج بالا مشخص می‌شود که این روش در حجم داده‌های پایین (مثل 100 بایت یا 1000 بایت) عملکرد مورد انتظار را نخواهد داشت. پس این نکته را نیز در هنگام پیاده سازی به این روش مد نظر داشته باشید.

 
مطالب
React 16x - قسمت 32 - React Hooks - بخش 3 - نکات ویژه‌ی برقراری ارتباط با سرور
در قسمت‌های 22 تا 25 این سری، روش برقراری ارتباط با سرور را در برنامه‌های React، توسط کتابخانه‌ی معروف Axios، بررسی کردیم. در این قسمت می‌خواهیم همان نکات را زمانیکه قرار است از کامپوننت‌های تابعی، به همراه useState hook و useEffect hook استفاده کنیم، مرور نمائیم.


برپایی پیش‌نیازها

در اینجا نیز از همان برنامه‌ای که در قسمت 30، برای بررسی مثال‌های React hooks ایجاد کردیم، استفاده خواهیم کرد. فقط در آن، کتابخانه‌ی Axios را نیز نصب می‌کنید. به همین جهت در ریشه‌ی پروژه‌ی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
برنامه‌ی backend مورد استفاده هم همان برنامه‌ای است که از قسمت 22 شروع به توسعه‌ی آن کردیم و کدهای کامل آن‌را از پیوست‌های انتهای بحث، می‌توانید دریافت کنید. این برنامه که در مسیر شروع شده‌ی با https://localhost:5001/api قرار می‌گیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شده‌است. برای راه اندازی آن، به پوشه‌ی این برنامه، مراجعه کرده و فایل dotnet_run.bat آن‌را اجرا کنید، تا endpointهای REST Api آن قابل دسترسی شوند. برای مثال باید بتوان به مسیر https://localhost:5001/api/posts آن در مرورگر دسترسی یافت.
در ادامه می‌خواهیم در برنامه‌ی React خود، لیست مطالب برنامه‌ی backend را از سرور دریافت کرده و نمایش دهیم. همچنین یک search box را به همراه دکمه‌های search و clear نیز به آن اضافه کنیم.


دریافت اطلاعات اولیه از سرور، درون useEffect Hook

پس از نصب پیش‌نیازها و راه اندازی برنامه‌ی backend، در ابتدا فایل src\config.json را جهت درج مشخصات آدرس REST Api آن، ایجاد می‌کنیم:
{
   "apiUrl": "https://localhost:5001/api"
}
سپس فایل جدید src\components\part03\Search.jsx را جهت توسعه‌ی کامپوننت جستجوی این قسمت ایجاد می‌کنیم و ساختار ابتدایی آن‌را با import وابستگی‌های React و مسیر فوق، به صورت یک function که در همان محل قابل export است، ایجاد می‌کنیم، تا فعلا یک React.Fragment را بازگشت دهد:
import React from "react";
import {apiUrl} from "../../config.json";

export default function App() {
  return <></>;
}
بر این اساس، کامپوننت App فایل index.js را به صورت زیر از کامپوننت App فوق، تامین خواهیم کرد:
import App from "./components/part03/Search";
اکنون می‌خواهیم اولین درخواست خود را به سمت backend server ارسال کنیم. برای این منظور در کامپوننت‌های تابعی، از useEffect Hook استفاده می‌شود؛ چون کار با یک API خارجی نیز یک side effect محسوب می‌گردد. بنابراین متد useEffect را import کرده و سپس آن‌را بالای return، فراخوانی می‌کنیم. درون آن نیاز است اطلاعات را از سرور دریافت کنیم و برای اینکار از کتابخانه‌ی axios که آن‌را در قسمت 23 معرفی کردیم، استفاده خواهیم کرد. به همین جهت import آن‌را نیز در این ماژول خواهیم داشت:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  useEffect(() => {
    axios
      .get(apiUrl + "/posts/search?query=")
      .then(response => console.log(response.data));
  });
  return <></>;
}
در اینجا با استفاده از متد get کتابخانه‌ی axios، درخواستی را به آدرس https://localhost:5001/api/posts/search، با یک کوئری استرینگ خالی، ارسال کرده‌ایم تا تمام داده‌ها را بازگشت دهد. روش قدیمی استفاده‌ی از axios را که با استفاده از Promiseها و متد then آن است، در اینجا ملاحظه می‌کنید که خروجی خاصیت data شیء response دریافتی را لاگ کرده‌است:


اکنون می‌خواهیم این اطلاعات دریافتی را در برنامه‌ی خود نیز نمایش دهیم. به همین جهت نیاز است تا response.data را درون state کامپوننت جاری قرار داده و در حین رندر کامپوننت، با تشکیل حلقه‌ای بر روی آن، اطلاعات نهایی را نمایش دهیم. بنابراین نیاز به useState Hook خواهیم داشت که ابتدا آن‌را import کرده و سپس آن‌را تعریف و در قسمت then، فراخوانی می‌کنیم:
import axios from "axios";
import React, { useEffect, useState } from "react";

import { apiUrl } from "../../config.json";

export default function App() {
  const [results, setResults] = useState([]);

  useEffect(() => {
    axios.get(apiUrl + "/posts/search?query=").then(response => {
      console.log(response.data);
      setResults(response.data);
    });
  });
چون اطلاعات بازگشتی به صورت یک آرایه‌است، مقدار اولیه‌ی متد useState را با یک آرایه‌ی خالی مقدار دهی کرده‌ایم. سپس برای مقدار دهی متغیر results موجود در state، به متد setResults تعریف شده‌ی توسط useState، مقدار response.data را ارسال می‌کنیم. در این حالت اگر برنامه را ذخیره کرده و اجرا کنید .... برنامه و همچنین مرورگر، هنگ می‌کنند!


همانطور که مشاهده می‌کنید، یک حلقه‌ی بی پایان در اینجا رخ داده‌است! برای پایان آن، مجبور خواهیم شد ابتدا کنسول اجرایی برنامه‌ی React را به صورت دستی خاتمه داده و سپس مرورگر را نیز refresh کنیم تا این حلقه، خاتمه پیدا کند.
علت این مشکل را در قسمت 30 بررسی کردیم؛ effect method تابع useEffect (همان متد در برگیرنده‌ی قطعه کدهای axios.get در اینجا)، پس از هربار رندر کامپوننت، یکبار دیگر نیز اجرا می‌شود. یعنی این متد، هر دو حالت componentDidMount و componentDidUpdate کامپوننت‌های کلاسی را با هم پوشش می‌دهد و چون در اینجا setState را با فراخوانی متد setResults داریم، یعنی درخواست رندر مجدد کامپوننت انجام شده‌است و پس از آن، مجددا effect method فراخوانی می‌شود و ... این حلقه هیچ‌گاه خاتمه نخواهد یافت. به همین جهت مرورگر و برنامه، هر دو با هم هنگ می‌کنند!

در این برنامه فعلا می‌خواهیم که فقط در حالت componentDidMount، کار درخواست اطلاعات از backend صورت گیرد. به همین جهت پارامتر دوم متد useEffect را با یک آرایه‌ی خالی مقدار دهی می‌کنیم:
  useEffect(() => {
   // ...
  }, []);
تا اینجا موفق شدیم متد setResults را تنها در اولین بار نمایش کامپوننت، فراخوانی کنیم که در نتیجه‌ی آن، متغیر results موجود در state، مقدار دهی شده و همچنین کار رندر مجدد کامپوننت در صف قرار می‌گیرد. بنابراین مرحله‌ی بعد، تکمیل قسمت return کامپوننت تابعی است تا آرایه‌ی results را نمایش دهد:
//...

export default function App() {
  // ...
  return (
    <>
      <table className="table">
        <thead>
          <tr>
            <th>Title</th>
          </tr>
        </thead>
        <tbody>
          {results.map(post => (
            <tr key={post.id}>
              <td>{post.title}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </>
  );
}
در اینجا ابتدا یک فرگمنت را توسط </><> تعریف کرده‌ایم و سپس در داخل آن می‌توان المان‌های فرزند را قرار داد. سپس برای ایجاد trهای جدول، یک حلقه را توسط results.map، بر روی عناصر دریافتی از آرایه‌ی مطالب، تشکیل داده‌ایم. چون این حلقه بر روی trهای پویا تشکیل می‌شود، هر tr، نیاز به یک key دارد، تا در DOM مجازی React قابل شناسایی و ردیابی شود که در آخر یک چنین شکلی را ایجاد می‌کند:



استفاده ازAsync/Await  برای دریافت اطلاعات، درون یک  useEffect Hook

اکنون می‌خواهیم درون effect method یک useEffect Hook، روش قدیمی استفاده‌ی از callbackها و متد then را برای دریافت اطلاعات، با روش جدیدتر async/await که در قسمت 23 آن‌را بیشتر بررسی کردیم، جایگزین کنیم.
  useEffect(async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  }, []);
خروجی متد axios.get، یک شیء Promise است که نتیجه‌ی عملیات async را بازگشت می‌دهد. در جاوا اسکریپت مدرن، می‌توان از واژه‌ی کلیدی await برای دسترسی به شیء response دریافتی از آن، استفاده کرد. سپس هر جائیکه از واژه‌ی کلیدی await استفاده می‌شود، متد جاری را باید با واژه‌ی کلیدی async نیز مزین کرد. با انجام اینکار و اجرای برنامه، اخطار زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود؛ هرچند نتیجه نهایی هم هنوز نمایش داده می‌شود:
Warning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise.
این اخطار به این معنا است که effect function تعریف شده را نمی‌توان به صورت async تعریف کرد و از چنین قابلیتی پشتیبانی نمی‌شود. یک effect function حداکثر می‌تواند یک متد دیگر را بازگشت دهد (و یا هیچ چیزی را بازگشت ندهد) که نمونه‌ی آن‌را در قسمت 30، با متدهایی که کار پاکسازی منابع را انجام می‌دادند، بررسی کردیم. اگر متدی را مزین به واژه‌ی کلیدی async کردیم، یعنی این متد در اصل یک Promise را بازگشت می‌دهد؛ اما یک effect function، حداکثر یک تابع دیگر را می‌تواند بازگشت دهد تا componentWillUnmount را پیاده سازی کند.

برای رفع این مشکل، روش توصیه شده، ایجاد یک تابع مجزای async و سپس فراخوانی آن درون effect function است:
  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    const { data } = await axios.get(apiUrl + "/posts/search?query=");
    console.log(data);
    setResults(data);
  };
مشکل یا محدودیتی برای ایجاد متدهای async، در خارج از یک effect function وجود ندارد. به همین جهت اعمالی را که نیاز به Async/Await دارند، در این متدهای مجزا انجام داده و سپس می‌توان آن‌ها را درون effect function، به نحوی که ملاحظه می‌کنید، فراخوانی کرد. با این تغییر، هنوز هم اطلاعات نهایی، بدون مشکل دریافت می‌شوند، اما دیگر اخطاری در کنسول توسعه دهندگان مرورگر درج نخواهد شد.


پیاده سازی componentDidUpdate با یک useEffect Hook، جهت انجام جستجوهای پویا

تا اینجا با اضافه کردن پارامتر دومی به متد useEffect، رویداد componentDidUpdate آن‌را از کار انداختیم، تا برنامه با هربار فراخوانی setState و اجرای مجدد effect function، در یک حلقه‌ی بی‌نهایت وارد نشود. اکنون این سؤال مطرح می‌شود که اگر یک textbox را برای جستجوی در عناوین نمایش داده شده، در بالای جدول آن قرار دهیم، نیاز است با هربار تغییر ورودی آن، کار فراخوانی مجدد effect function صورت گیرد، تا بتوان نتایج جدیدتری را از سرور دریافت و به کاربر نشان داد؛ این مشکل را چگونه باید حل کرد؟
برای دریافت عبارت وارد شده‌ی توسط کاربر و جستجو بر اساس آن، ابتدا متغیر state و متد تنظیم آن‌را با استفاده از useState Hook و یک مقدار اولیه‌ی دلخواه تنظیم می‌کنیم:
export default function App() {
  // ...
  const [query, setQuery] = useState("Title");
سپس المان textbox زیر را هم به بالای المان جدول، اضافه می‌کنیم:
<input
  type="text"
  name="query"
  className="form-control my-3"
  placeholder="Search..."
  onChange={event => setQuery(event.target.value)}
  value={query}
/>
این کنترل توسط رویداد onChange، عبارت تایپ شده را به متد setQuery ارسال کرده و در نتیجه‌ی آن، کار تنظیم متغیر query در state کامپوننت جاری، صورت می‌گیرد. همچنین با تنظیم value={query}، سبب خواهیم شد تا این کنترل، به یک المان کنترل شده‌ی توسط state تبدیل شود و در ابتدای نمایش فرم، مقدار ابتدایی useState را نمایش دهد.
اکنون که متغیر query دارای مقدار شده‌است، می‌توان از آن در متد axios.get، به نحو زیر و با ارسال یک کوئری استرینگ به سمت سرور، استفاده کرد:
const { data } = await axios.get(
   `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
);
استفاده از تابع encodeURIComponent، سبب می‌شود تا اگر کاربر برای مثال "Text 1" را وارد کرد، فاصله‌ی بین دو عبارت، به درستی encode شده و یک کوئری مانند https://localhost:5001/api/posts/search?query=Title%201 به سمت سرور ارسال گردد.

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، با تایپ در textbox جستجو، تغییری در نتایج حاصل نمی‌شود؛ چون effect function تعریف شده که سبب اجرای مجدد axios.get می‌شود، طوری تنظیم شده‌است که فقط یکبار، آن‌هم پس از رندر اولیه‌ی کامپوننت، اجرا شود. برای رفع این مشکل، با مقدار دهی آرایه‌ای که به عنوان پارامتر دوم متد useEffect تعریف شده، می‌توان اجرای مجدد effect function آن‌را وابسته‌ی به تغییرات متغیر query در state کامپوننت کرد:
  useEffect(() => {
    getResults();
  }, [query]);
اکنون اگر برنامه را ذخیره کرده و اجرا کنید، با هربار ورود اطلاعات درون textbox جستجو، یک کوئری جدید به سمت سرور ارسال شده و نتیجه‌ی جستجوی انجام شده، به صورت یک جدول رندر می‌شود:



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


تا اینجا کاربر با هر حرفی که درون textbox جستجو وارد می‌کند، یک کوئری، به سمت سرور ارسال خواهد شد. برای کاهش آن می‌توان یک دکمه‌ی جستجو را در کنار این textbox قرار داد تا تنها پس از کلیک بر روی آن، این جستجو صورت گیرد.
برای پیاده سازی این قابلیت، ابتدا وابستگی به query را از متد useEffect حذف می‌کنیم، تا دیگر با تغییر اطلاعات textbox، متد callback آن اجرا نشود (پارامتر دوم آن‌را مجددا به یک آرایه‌ی خالی تنظیم می‌کنیم). سپس یک دکمه را که از نوع button است و رویداد onClick آن به getResults اشاره می‌کند، در بالای جدول نتایج مطالب، قرار می‌دهیم:
<button
  className="btn btn-primary"
  type="button"
  onClick={getResults}
>
  Search
</button>
تا اینجا اگر کاربر اطلاعاتی را وارد کرده و سپس بر روی دکمه‌ی Search فوق کلیک کند، نتایج جستجوی خودش را در جدول ذیل آن مشاهده می‌کند. اکنون می‌خواهیم این امکان را به کاربران بدهیم که با فشردن دکمه‌ی enter درون textbox جستجو، همین قابلیت جستجو را در اختیار داشته باشند؛ تا دیگر الزامی به کلیک بر روی دکمه‌ی Search، نباشد. برای اینکار تنها کافی است، کل مجموعه‌ی textbox و دکمه را درون یک المان form قرار دهیم و نوع button را نیز به submit تغییر دهیم. سپس onClick دکمه را حذف کرده و بجای آن رویداد onSubmit فرم را پیاده سازی می‌کنیم:
<form onSubmit={handleSearch}>
  <div className="input-group my-3">
    <label htmlFor="query" className="form-control-label sr-only"></label>
    <input
type="text"
id="query"
name="query"
className="form-control"
placeholder="Search ..."
onChange={event => setQuery(event.target.value)}
value={query}
    />
    <div className="input-group-append">
<button className="btn btn-primary" type="submit">
  Search
</button>
    </div>
  </div>
</form>
در اینجا یک المان فرم، به همراه یک textbox و button از نوع submit تعریف شده‌اند. رویداد onSubmit نیز به متد منتسب به متغیر handleSearch، متصل شده‌است تا با فشردن دکمه‌ی enter توسط کاربر در این textbox، کار جستجوی مجدد، صورت گیرد:
  const handleSearch = event => {
    event.preventDefault();
    getResults();
  };
تا اینجا اگر برنامه را ذخیره کرده و "Text 1" را در textbox جستجو، وارد کرده و enter کنیم، همانند تصویر فوق، رکورد متناظری از سرور دریافت و نمایش داده می‌شود.


افزودن قابلیت پاک کردن textbox جستجو و معرفی useRef Hook

در ادامه می‌خواهیم یک دکمه‌ی جدید را در کنار دکمه‌ی Search، اضافه کنیم تا با کلیک کاربر بر روی آن، نه فقط محتوای وارد شده‌ی در textbox پاک شود، بلکه focus نیز به آن منتقل گردد. برای پاک کردن textbox، فقط کافی است متد setQuery را با یک رشته‌ی خالی ارسالی به آن فراخوانی کنیم. اما برای انتقال focus به textbox، نیاز به داشتن ارجاع مستقیمی به آن المان وجود دارد که با مفهوم آن در قسمت 18 آشنا شدیم: «برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت ایجاد کرده و آن‌را با React.RefObject مقدار دهی اولیه کرده و سپس ویژگی ref المان مدنظر را به این RefObject تنظیم می‌کنیم». برای انجام یک چنین کاری در اینجا، Hook ویژه‌ای به نام useRef معرفی شده‌است. بنابراین برای پیاده سازی این نیازمند‌ی‌ها، ابتدا دکمه‌ی Clear را در کنار دکمه‌ی Search قرار می‌دهیم:
<button
  type="button"
  onClick={handleClearSearch}
  className="btn btn-info"
>
  Clear
</button>
سپس رویداد onClick آن‌را به متد منتسب به متغیر handleClearSearch، مرتبط می‌کنیم:
import React, { useEffect, useRef, useState } from "react";
// ...

export default function App() {
  // ...
  const searchInputRef = useRef();


  const handleClearSearch = () => {
    setQuery("");
    searchInputRef.current.focus();
  };
در اینجا ابتدا useRef را import کرده‌ایم، تا توسط آن بتوان یک متغیر از نوع React.MutableRefObject را ایجاد کرد. سپس در متد منتسب به handleClearSearch، ابتدا با فراخوانی setQuery، مقدار query را در state کامپوننت، پاک کرده و سپس به کمک این شیء Ref، دسترسی مستقیمی به شیء textbox یافته و متد focus آن‌را فراخوانی می‌کنیم (شیء current آن، معادل DOM Element متناظر است).
البته این searchInputRef برای اینکه دقیقا به textbox تعریف شده اشاره کند، باید آن‌را به ویژگی ref المان، انتساب داد:
<input
  type="text"
  id="query"
  name="query"
  className="form-control"
  placeholder="Search ..."
  onChange={event => setQuery(event.target.value)}
  value={query}
  ref={searchInputRef}
/>
تا اینجا اگر برنامه را ذخیره کرده و اجرا کنیم، با کلیک بر روی دکمه‌ی Clear، متن textbox جستجو حذف شده و سپس کرسر مجددا به همان textbox برای ورود اطلاعات، منتقل می‌شود.


نمایش «لطفا منتظر بمانید» در حین دریافت اطلاعات از سرور


البته در اینجا با هر بار کلیک بر روی دکمه‌ی جستجو، نتیجه‌ی نهایی به سرعت نمایش داده می‌شود؛ اما اگر سرعت اتصال کاربر کمتر باشد، با یک وقفه این امر رخ می‌دهد. به همین جهت بهتر است یک پیام «لطفا منتظر بمانید» را در این حین به او نمایش دهیم. به همین جهت در ابتدا state مرتبطی را به کامپوننت اضافه می‌کنیم:
const [loading, setLoading] = useState(false);
تا با فراخوانی متد setLoading آن بتوان سبب رندر مجدد UI شد و پیامی را نمایش داد و یا مخفی کرد:
  const getResults = async () => {
    setLoading(true);
    const { data } = await axios.get(
      `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
    );
    console.log(data);
    setResults(data);
    setLoading(false);
  };
متد setLoading در ابتدای متد منتسب به متغیر getResults، مقدار متغیر loading را در state به true تنظیم می‌کند و در پایان عملیات، به false. اکنون بر این اساس می‌توان UI متناظری را نمایش داد:
      {loading ? (
        <div className="alert alert-info">Loading results...</div>
      ) : (
        <table className="table">
          <thead>
            <tr>
              <th>Title</th>
            </tr>
          </thead>
          <tbody>
            {results.map(post => (
              <tr key={post.id}>
                <td>{post.title}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
در اینجا با استفاده از یک ternary operator، اگر loading به true تنظیم شده باشد، یک div به همراه عبارت Loading results، نمایش داده می‌شود؛ در غیراینصورت، جدول اطلاعات مطالب، نمایش داده خواهد شد.

برای آزمایش آن می‌توان سرعت اتصال را در برگه‌ی شبکه‌ی ابزارهای توسعه دهندگان مرورگر، تغییر داد:



مدیریت خطاها در حین اعمال async

آخرین امکانی را که به این مطلب اضافه خواهیم کرد، مدیریت خطاهای اعمال async است که با try/catch صورت می‌گیرد:
// ...

export default function App() {
  // ...
  const [error, setError] = useState(null);

  // ...

  const getResults = async () => {
    setLoading(true);

    try {
      const { data } = await axios.get(
        `${apiUrl}/posts/search?query=${encodeURIComponent(query)}`
      );
      console.log(data);
      setResults(data);
    } catch (err) {
      setError(err);
    }

    setLoading(false);
  };
در حین فراخوانی await axios.get، اگر خطایی رخ دهد، این کتابخانه استثنایی را صادر خواهد کرد که می‌توان به جزئیات آن در بدنه‌ی catch نوشته شده دسترسی یافت و برای مثال آن‌را به کاربر نمایش داد. برای این منظور ابتدا state مخصوص آن‌را ایجاد می‌کنیم و سپس توسط فراخوانی متد setError آن، کار رندر مجدد کامپوننت را در صف انجام قرار خواهیم داد.در نهایت برای نمایش آن می‌توان یک div را به پایین جدول اضافه نمود:
{error && <div className="alert alert-warning">{error.message}</div>}
برای آزمایش آن، برنامه‌ی backend را که در حال اجرا است، خاتمه دهید و سپس در برنامه سعی کنید به آن متصل شوید:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-30-part-03-frontend.zip و sample-30-part-03-backend.zip
مطالب
تفاوت Desktop Application با Web Application
در هنگام گفتگو با افراد مختلفی که در پروژه‌های توسعه نرم افزار، نقش‌های مختلفی را دارا می‌باشند، یکی از جالب‌ترین و اساسی‌ترین بحث‌ها تفاوت بین Desktop App و Web App می‌باشد، و این که پروژه بر اساس کدام مدل باید نوشته شود.

در اینترنت و در منابع معتبر، تفسیر‌های متفاوتی از این دو وجود دارد، که گاه دقیقا با نظر من یکی بوده و گاه تا 180 درجه بر عکس هستند، آنچه که در ادامه می‌خوانید می‌تواند لزوما نظر شما نباشد.
گروهی از افراد بر این باور هستند که اجرای برنامه در محیط مرورگر (ظاهر مرورگر و نه Sandbox آن)، یکی از ملاک‌های ما بین Desktop App  و Web App است، گروهی دیگر نیز اجرا شدن برنامه بر روی بستر اینترنت و یا شبکه‌ی محلی را جزو ملاک‌ها می‌دانند، و گروهی دیگر نیز زبان برنامه نویسی برنامه را ملاک می‌دانند، برای مثال اگر با HTML/JS باشد Web App است، اگر نه Desktop App است.
اما آنچه که در عمل می‌تواند تفاوت بین یک Desktop App را با یک Web App مشخص کند، رفتار و عملکرد خود آن برنامه است، نه بستر اجرای آن و این که آن رفتار منتج شده از چه کدی و چه زبان برنامه نویسی ای است.
اگر کمی دقیق به مطلب نگاه کنیم، می‌بینیم این که یک برنامه در چارچوب ظاهری یک مرورگر (نه Sandbox آن) اجرا شود، اصلا مقوله ای اهمیت دار نیست، کما این که برای مثال Silverlight اجازه می‌دهد، برنامه هم در داخل مرورگر و هم در بیرون از آن اجرا شود، و این کار با یک کلیک امکانپذیر است، آیا با همین یک کلیک برنامه از Web App به Desktop App تبدیل می‌شود یا بالعکس ؟
آیا یک برنامه مبتنی بر دلفی که تا همین یک ساعت پیش بر روی شبکه محلی در حال اجرا بوده، با انتقال پیدا کردن آن بر روی شبکه‌ی اینترنت، تبدیل به یک Web App می‌شود؟
آیا اگر ما با HTML/JS یک برنامه Native برای ویندوز فون بنویسیم که تک کاربره آفلاین باشد و اصلا سروری هم نداشته باشد، آیا Web App نوشته ایم ؟
اصلی‌ترین تفاوت مابین Web App و Desktop App که به تفاوت در عملکرد آنها و مزایا و معایب آنها منجر می‌شود، این است که انجام کارهایی که اپراتور با آنها در سمت کلاینت و سیستم مشتری سر و کار دارد، در کجا صورت می‌پذیرد؟
برای مثال در نظر بگیرید که یک دیتاگرید داریم که دارای Paging است، و ما از Page اول به Page بعدی می‌رویم، در یک Desktop App تنها اطلاعات از سرور گرفته می‌شود، و ترسیم خطوط و ستون‌ها و ردیف‌ها و ظاهر نمایشی دیتاگرید بر عهده کلاینت است، برای مثال اگر ستون قیمت داشته باشیم، و بخواهیم برای ردیف هایی که قیمت آنها زیر 10000 ریال است، قیمت به شکل سبز رنگ نمایش داده شود و برای بقیه ردیف‌ها به رنگ قرمز باشد، پردازش این مسئله و این if به عهده کلاینت است، اما در یک Web App، علاوه بر اطلاعات، تعداد زیادی tag‌های مختلف، مانند table - tr - td و ... نیز به همراه اطلاعات آورده می‌شوند، که وظیفه نمایش ظاهری اطلاعات را بر عهده دارند، و آن if مثال ما یعنی رنگ سبز و قرمز در سمت سرور مدیریت شده است، و کلاینت در اینجا نمایش دهنده‌ی آن چیزی است که به صورت آماده از سرور آورده شده است.
در برنامه‌های Desktop آنچه که در سمت سرور وجود دارد، برای مثال یک WCF Service یا ASP.NET Web API است که فقط به رد و بدل کردن اطلاعات می‌پردازد، اما در Web App‌ها در سمت سرور ASP.NET Web Forms، ASP.NET MVC و PHP وجود دارند که علاوه بر اطلاعات برای کلاینت شما ظاهر صفحات را نیز آماده می‌کنند، و ظاهر اصلی صفحات از سمت سرور به سیستم مشتری ارسال می‌شوند، اگر چه که ممکن است در سمت کلاینت تغییراتی را داشته باشند.
به هر میزان رفتار برنامه ما شبیه به حالت اول باشد، برنامه ما Desktop App بوده و به هر میزان برنامه ما به حالت دوم نزدیک‌تر باشد، برنامه ما Web App است.
مزیت اصلی Web App‌ها در عدم انداختن بار زیاد بر روی دوش کلاینت‌های بعضا نحیف بوده، و عملا کلاینت به علت این که کار خاصی را انجام نمی‌دهد، پیش نیاز نرم افزاری و یا سخت افزاری خاصی احتیاج ندارد، و این مورد Web App‌ها را به یک گزینه ایده آل برای وب سایت هایی تبدیل کرده است که با عموم مردم در ارتباطند، زیرا که امکان ارائه آسان برنامه وجود دارد و تقریبا همه می‌توانند از آن استفاده کنند.
با توجه به شناخت عموم از برنامه‌های Web App به توضیح بیشتر برنامه‌های Desktop App می‌پردازم.
مزیت اصلی Desktop App‌ها در سرعت عمل بالاتر(به علت این که فقط دیتا را رد و بدل می‌کند)، توانایی بیشتر در استفاده از منابع سیستمی مانند سرویس نوشتن، و امکانات محلی مانند ارائه Notification و ... است، و در کنار آن برای مثال یک Desktop App می‌تواند به نحوی طراحی شود که به صورت Offline نیز کار کند.
این مزیت‌ها باعث می‌شود که Desktop App‌ها گزینه ای مناسب برای برنامه‌های سازمانی باشند.
ضعفی که از گذشته در Desktop App‌ها وجود داشته است، که البته به معماری Desktop App بر نمی‌گردد، بلکه متاثر از امکانات است، عدم Cross Platform بودن آنها بوده است، تا آنجا که Desktop App در نظر خیلی از افراد همان نوشتن برنامه برای سیستم عامل ویندوز است.
با توجه به رویکرد جدی ای که در طول دو سال اخیر برای نوشتن برنامه Desktop App به شکل Cross Platform رخ داده است، خوشبختانه این مشکل حل شده است و اکنون لااقل دو راهکار جدی برای نوشتن یک برنامه Cross Platform با ویژگی‌های Desktop وجود دارد، که یکی از آنها راه حل‌های مبتنی بر HTML/JS است و دیگری راه حل‌های مبتنی بر C#/XAML
در راه حل‌های مبتنی بر HTML/JS در صورتی که شما برنامه را به شکل Web App طراحی نکرده باشید، و برای مثال در آن از ASP.NET Web Forms و ASP.NET MVC، PHP و ... استفاده نکرده باشید، می‌توانید یک خروجی کاملا Native با تمامی ویژگی‌های Desktop App برای انواع پلتفرم‌ها بگیرید.
استفاده از فریم ورک هایی که با طراحی Desktop App سازگار هستند، مانند Angular JS، Kendo UI و Ext JS، Jay-data و ... و استفاده از مدل طراحی Single Page Application می‌تواند سیستم کدنویسی ای ساده را فراهم آورد، که در آن شما با یک بار نوشتن برنامه می‌توانید خروجی اکثر پلتفرم‌های مطرح را داشته باشید، اعم از ویندوز فون، اندروید، iOS و ویندوز
امروزه حتی مرورگر‌ها با فراهم آوردن امکاناتی مانند Client side databases و Manifest based deployment اجازه نوشتن برنامه Desktop با HTML/JS را که حتی می‌تواند Offline کار کند را به شما ارائه می‌کنند.
در کنار این راهکار، استفاده از C#/XAML برای نوشتن برنامه برای اکثر پلتفرم‌های مطرح بازار اعم از اندروید، iOS و Windows Phone و ویندوز، نیز به عنوان راهکاری دیگر قابلیت استفاده را دارا است.
حرکت پر شتاب و پر انرژی جهانی برای توسعه Cross Platform Desktop Development، خوشبختانه توانسته است تا حد زیادی امتیاز نوشتن برنامه‌های Desktop را در سیستم‌های Enterprise بالا ببرد.
مطالب
تولید فایل‌های اکسل حرفه‌ای بدون نیاز به نصب مجموعه‌ی آفیس

عموما بر روی سرورهای برنامه‌های وب، نرم افزار خاصی نصب نمی‌شود. برای مثال اگر نیاز به تولید فایل اکسل بر روی سرور باشد، سرور دار بعید است که آفیس را برای شما نصب کند و همچنین مایکروسافت هم این یک مورد را اصلا توصیه و پشتیبانی نمی‌کند (ایجاد چندین وهله از برنامه آفیس (تعامل با اشیاء COM) بر روی سرور توسط یک برنامه‌ی وب چند کاربره).
اگر سایت‌ها را هم جستجو کنید پر است از مقالاتی مانند تبدیل GridView به اکسل ... که تنها هنر آن‌ها انتخاب قسمت table مانند GridView و رندر کردن آن در مرورگر با پسوندی به نام xls یا xlsx است. به عبارتی فایل نهایی تولید شده استاندارد نیست. فقط یک html table است با پسوند xls/xlsx که برنامه‌ی اکسل می‌داند به چه صورتی باید آن‌را باز کند (که گاها در این بین فارسی سازی آن مشکل ساز می‌شود). این فایل نهایی تولیدی عاری است از امکانات پیشرفته‌ و حرفه‌ای اکسل. برای مثال اضافه کردن فرمول به آن، تبدیل اطلاعات به نمودارهای اکسل به صورت خودکار، داشتن فایلی با چندین work sheet‌ مختلف، اعمال قالب‌های مختلف، صفحه بندی بهتر و غیره.
مایکروسافت از سال 2007 تولید فایل‌های آفیس را با معرفی استاندارد OpenXML که توسط مؤسسه ایزو هم پذیرفته شده، بسیار ساده‌تر کرده است. OpenXML SDK‌ در دسترس است و توسط آن می‌توان فایل‌های اکسل حرفه‌ای را بدون نیاز به نصب مجموعه‌ی آفیس تولید کرد. کار کردن با OpenXML SDK هم در نگاه اول شاید ساده به نظر برسد اما آن هم ریزه کاری‌های خاص خودش را دارد که نمونه‌ای از آن‌را در مطلب "تولید فایل Word بدون نصب MS Word بر روی سرور" می‌توانید مشاهده کنید. به عبارتی این مجموعه جهت نوشتن کتابخانه‌های ویژه‌ی شما باز است ...
در این بین یکی از حرفه‌ای‌ترین کتابخانه‌هایی که امکانات تولید فایل‌های اکسل را به کمک OpenXML SDK‌ سهولت می‌بخشد، کتابخانه‌ی سورس باز EPPlus است:


مثالی در مورد نحوه‌ی استفاده از آن:
می‌خواهیم یک DataTable را به یک فایل اکسل واقعی (نه یک html table با پسوند xlsx) تبدیل کنیم با این شرایط که یکی از قالب‌های جدید آفیس به آن اعمال شود؛ جمع کل یکی از ستون‌ها توسط اکسل محاسبه گردیده و همچنین عرض دقیق ستون‌ها نیز در برنامه تنظیم گردد. نموداری نیز به صورت خودکار این اطلاعات را نمایش دهد:




using System.Data;
using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Drawing.Chart;
using OfficeOpenXml.Style;
using OfficeOpenXml.Table;

namespace EPPlusTest
{
class Program
{
static void Main(string[] args)
{
var newFile = new FileInfo("Test.xlsx");
if (newFile.Exists)
{
newFile.Delete();
}

//ایجاد یک سری اطلاعات دلخواه
var table = createDt();

using (var package = new ExcelPackage(newFile))
{
// اضافه کردن یک ورک شیت جدید
ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("مخارج");

//اضافه کردن یک جدول جدید از دیتاتیبل دریافتی
worksheet.Cells["A1"].LoadFromDataTable(table, true, TableStyles.Dark9);

//نمایش جمع ستون هزینه‌های ماه‌ها
var tbl = worksheet.Tables[0];
//زیر آخرین ردیف یک سطر اضافه می‌کند
tbl.ShowTotal = true;
//فرمول نحوه‌ی محاسبه جمع ستون انتساب داده می‌شود
tbl.Columns[1].TotalsRowFunction = RowFunctions.Sum;

//تعیین عرض ستون‌های جدول
worksheet.Column(1).Width = 14;
worksheet.Column(2).Width = 12;

//تنظیم متن هدر
worksheet.HeaderFooter.oddHeader.CenteredText = "مثالی از نحوه‌ی استفاده از ایی پی پلاس";

//می‌خواهیم سرستون‌ها در وسط ستون قرار گیرند
worksheet.Cells["A1"].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;
worksheet.Cells["B1"].Style.HorizontalAlignment = ExcelHorizontalAlignment.Center;

//افزودن یک نمودار جدید به شیت جاری
var chart = worksheet.Drawings.AddChart("chart1", eChartType.Pie3D);
chart.Title.Text = "نمودار هزینه‌های سال";
chart.SetPosition(Row: 2, RowOffsetPixels: 5, Column: 3, ColumnOffsetPixels: 5);
chart.SetSize(PixelWidth: 320, PixelHeight: 360);
chart.Series.Add("B2:B13", "A2:A13");
chart.Style = eChartStyle.Style26;

//تنظیم یک سری خواص فایل نهایی
package.Workbook.Properties.Title = "مثالی از ایی پی پلاس";
package.Workbook.Properties.Author = "وحید";
package.Workbook.Properties.Subject = "ایجاد فایل اکسل بدون نرم افزار اکسل";

//تنظیم نحوه‌ی نمایش فایل زمانیکه در نرم افزار اکسل گشوده می‌شود
worksheet.View.PageLayoutView = true;
worksheet.View.RightToLeft = true;

// ذخیر سازی کلیه موارد اعمالی در فایل
package.Save();
}
}

private static DataTable createDt()
{
var table = new DataTable("مخارج");
table.Columns.Add("ماه", typeof(string));
table.Columns.Add("هزینه", typeof(decimal));

table.Rows.Add("فروردین", 100);
table.Rows.Add("اردیبهشت", 250);
table.Rows.Add("خرداد", 80);
table.Rows.Add("تیر", 300);
table.Rows.Add("مرداد", 200);
table.Rows.Add("شهریور", 150);
table.Rows.Add("مهر", 250);
table.Rows.Add("آبان", 200);
table.Rows.Add("آذر", 400);
table.Rows.Add("دی", 100);
table.Rows.Add("بهمن", 130);
table.Rows.Add("اسفند", 80);
return table;
}
}
}

نظرات مطالب
Blazor 5x - قسمت پنجم - مبانی Blazor - بخش 2 - کامپوننت‌ها
نکته تکمیلی: برای کارآمدی بهتر لیست‌ها و جداول به جای استفاده مستقیم از foreach یا for از Virtualize استفاده نمایید:
<Virtualize Items="cars" Context="car">
  <tr>
    <td>@car.Id</td>
    <td>@car.Name</td>
    <td>@car.Cost</td>
  </tr>
</Virtualize>
برای پروژه ما که در یک جدول نزدیک 5 هزار رکورد را نمایش میداد دو یا سه ثانیه طول میکشید که دیتای آماده را نمایش دهد با تغییر این بخش از کد الان بدون وقفه دیتا را نمایش میدهد
مطالب دوره‌ها
شروع به کار با RavenDB
پیشنیاز‌های بحث
- مروری بر مفاهیم مقدماتی NoSQL
- رده‌ها و انواع مختلف بانک‌های اطلاعاتی NoSQL
- چه زمانی بهتر است از بانک‌های اطلاعاتی NoSQL استفاده کرد و چه زمانی خیر؟

لطفا یکبار این پیشنیازها را پیش از شروع به کار مطالعه نمائید؛ چون بسیاری از مفاهیم پایه‌ای و اصطلاحات مرسوم دنیای NoSQL در این سه قسمت بررسی شده‌اند و از تکرار مجدد آن‌ها در اینجا صرفنظر خواهد شد.


RavenDB چیست؟

RavenDB یک بانک اطلاعاتی سورس باز NoSQL سندگرای تهیه شده با دات نت  است. ساختار کلی بانک‌های اطلاعاتی NoSQL سندگرا، از لحاظ نحوه ذخیره سازی اطلاعات، با بانک‌های اطلاعاتی رابطه‌ای متداول، کاملا متفاوت است. در اینگونه بانک‌های اطلاعاتی، رکوردهای اطلاعات، به صورت اشیاء JSON ذخیره می‌شوند. اشیاء JSON یا JavaScript Object Notation بسیار شبیه به anonymous objects سی شارپ هستند. JSON روشی است که توسط آن JavaScript اشیاء خود را معرفی و ذخیره می‌کند. به عنوان رقیبی برای XML مطرح است؛ نسبت به XML اندکی فشرده‌تر بوده و عموما دارای اسکیمای خاصی نیست و در بسیاری از اوقات تفسیر المان‌های آن به مصرف کننده واگذار می‌شود.
در JSON عموما سه نوع المان پایه مشاهده می‌شوند:
- اشیاء که به صورت {object} تعریف می‌شوند.
- مقادیر "key":"value" که شبیه به نام خواص و مقادیر آن‌ها در دات نت هستند.
- و آرایه‌ها به صورت [array]
همچنین ترکیبی از این سه عنصر یاد شده نیز همواره میسر است. برای مثال، یک key مشخص می‌تواند دارای مقداری حاوی یک آرایه یا شیء نیز باشد.
JSON: JavaScript Object Notation

document :{ 
   key: "Value",
   another_key: {
      name: "embedded object"
   },
   some_date: new Date(),
   some_number: 12
}

C# anonymous object

var Document = new { 
   Key= "Value",
   AnotherKey= new {
      Name = "embedded object"
   },
   SomeDate = DateTime.Now(),
   SomeNumber = 12
};
به این ترتیب می‌توان به یک ساختار دلخواه و بدون اسکیما، از هر سند به سند دیگری رسید. اغلب بانک‌های اطلاعاتی سندگرا، اینگونه اسناد را در زمان ذخیره سازی، به یک سری binary tree تبدیل می‌کنند تا تهیه کوئری بر روی آن‌ها بسیار سریع شود. مزیت دیگر استفاده از JSON، سادگی و سرعت بالای Serialize و Deserialize اطلاعات آن برای ارسال به کلاینت‌ها و یا دریافت آن‌ها است؛ به همراه فشرده‌تر بودن آن نسبت به فرمت‌های مشابه دیگر مانند XML.


یک نکته مهم
اگر پیشنیازهای بحث را مطالعه کرده باشید، حتما بارها با این جمله که دنیای NoSQL از تراکنش‌ها پشتیبانی نمی‌کند، برخورد داشته‌اید. این مطلب در مورد RavenDB صادق نیست و این بانک اطلاعاتی NoSQL خاص، از تراکنش‌ها پشتیبانی می‌کند. RavenDB در Document store خود ACID عملکرده و از تراکنش‌ها پشتیبانی می‌کند. اما تهیه ایندکس‌های آن بر مبنای مفهوم عاقبت یک دست شدن عمل می‌کند.


مجوز استفاده از RavenDB

هرچند مجموعه سرور و کلاینت RavenDB سورس باز هستند، اما این مورد به معنای رایگان بودن آن نیست. مجوز استفاده از RavenDB نوع خاصی به نام AGPL است. به این معنا که یا کل کار مشتق شده خود را باید به صورت رایگان و سورس باز ارائه دهید و یا اینکه مجوز استفاده از آن‌را برای کارهای تجاری بسته خود خریداری نمائید. نسخه استاندارد آن نزدیک به هزار دلار است و نسخه سازمانی آن نزدیک به 2800 دلار به ازای هر سرور.


شروع به نوشتن اولین برنامه کار با RavenDB

ابتدا یک پروژه کنسول ساده را آغاز کنید. سپس کلاس‌های مدل زیر را به آن اضافه نمائید:
using System.Collections.Generic;

namespace RavenDBSample01.Models
{
    public class Question
    {
        public string By { set; get; }
        public string Title { set; get; }
        public string Content { set; get; }

        public List<Comment> Comments { set; get; }
        public List<Answer> Answers { set; get; }

        public Question()
        {
            Comments = new List<Comment>();
            Answers = new List<Answer>();
        }
    }
}

namespace RavenDBSample01.Models
{
    public class Comment
    {
        public string By { set; get; }
        public string Content { set; get; }
    }
}

namespace RavenDBSample01.Models
{
    public class Answer
    {
        public string By { set; get; }
        public string Content { set; get; }
    }
}
سپس به کنسول پاور شل نیوگت در ویژوال استودیو مراجعه کرده و دستورات ذیل را جهت افزوده شدن وابستگی‌های مورد نیاز RavenDB، صادر کنید:
PM> Install-Package RavenDB.Client
PM> Install-Package RavenDB.Server
به این ترتیب بسته‌های کلاینت (مورد نیاز جهت برنامه نویسی) و سرور RavenDB به پروژه جاری اضافه خواهند شد (نگارش 2.5 در زمان نگارش این مطلب؛ جمعا نزدیک به 75 مگابایت).
اکنون به پوشه packages\RavenDB.Server.2.5.2700\tools مراجعه کرده و برنامه Raven.Server.exe را اجرا کنید تا سرور RavenDB شروع به کار کند. این سرور به صورت پیش فرض بر روی پورت 8080 اجرا می‌شود. از این جهت که در RavenDB نیز همانند سایر Document Stores مطرح، امکان دسترسی به اسناد از طریق REST API و Urlها وجود دارد.
البته لازم به ذکر است که RavenDB در 4 حالت برنامه کنسول (همین سرور فوق)، نصب به عنوان یک سرویس ویندوز NT، هاست شدن در IIS و حالت مدفون شده یا Embedded قابل استفاده است.

خوب؛ همین اندازه برای برپایی اولیه RavenDB کفایت می‌کند.
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
                               {
                                   Url = "http://localhost:8080"
                               }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    session.Store(new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test...."
                    });

                    session.SaveChanges();
                }
            }
        }
    }
}
اکنون کدهای برنامه کنسول را به نحو فوق برای ذخیره سازی اولین سند خود، تغییر دهید.
کار با ایجاد یک DocumentStore که به آدرس سرور اشاره می‌کند و کار مدیریت اتصالات را برعهده دارد، شروع خواهد شد. اگر نمی‌خواهید Url را درون کدهای برنامه مقدار دهی کنید، می‌توان از فایل کانفیگ برنامه نیز برای این منظور کمک گرفت:
<connectionStrings>
   <add name="ravenDB" connectionString="Url=http://localhost:8080"/>
</connectionStrings>
در این حالت باید خاصیت ConnectionStringName شیء DocumentStore را مقدار دهی نمود.
 سپس با ایجاد Session در حقیقت یک Unit of work آغاز می‌شود که درون آن می‌توان انواع و اقسام دستورات را صادر نمود و سپس در پایان کار، با فراخوانی SaveChanges، این اعمال ذخیره می‌گردند. در RavenDB یک سشن باید طول عمری کوتاه داشته باشد و اگر تعداد عملیاتی که در آن صادر کرده‌اید، زیاد است با خطای زیر متوقف خواهید شد:
 The maximum number of requests (30) allowed for this session has been reached.
البته این نوع محدودیت‌ها عمدی است تا برنامه نویس به طراحی بهتری برسد.

در یک برنامه واقعی، ایجاد DocumentStore یکبار در آغاز کار برنامه باید انجام گردد. اما هر سشن یا هر واحد کاری آن، به ازای تراکنش‌های مختلفی که باید صورت گیرند، بر روی این DocumentStore، ایجاد شده و سپس بسته خواهند شد. برای مثال در یک برنامه ASP.NET، در فایل Global.asax در زمان آغاز برنامه، کار ایجاد DocumentStore انجام شده و سپس به ازای هر درخواست رسیده، یک سشن RavenDB ایجاد و در پایان درخواست، این سشن آزاد خواهد شد.

برنامه را اجرا کنید، سپس به کنسول سرور RavenDB که پیشتر آن‌را اجرا نمودیم مراجعه نمائید تا نمایی از عملیات انجام شده را بتوان مشاهده کرد:
Raven is ready to process requests. Build 2700, Version 2.5.0 / 6dce79a Server started in 14,438 ms
Data directory: D:\Prog\RavenDBSample01\packages\RavenDB.Server.2.5.2700\tools\Database\System
HostName: <any> Port: 8080, Storage: Esent
Server Url: http://localhost:8080/
Available commands: cls, reset, gc, q
Request #   1: GET     -   514 ms - <system>   - 404 - /docs/Raven/Replication/Destinations
Request #   2: GET     -   763 ms - <system>   - 200 - /queries/?&id=Raven%2FHilo%2Fquestions&id=Raven%2FServerPrefixForHilo
Request #   3: PUT     -   185 ms - <system>   - 201 - /docs/Raven/Hilo/questions
Request #   4: POST    -   103 ms - <system>   - 200 - /bulk_docs
        PUT questions/1
زمانیکه سرور RavenDB در حالت دیباگ در حال اجرا باشد، لاگ کلیه اعمال انجام شده را در کنسول آن می‌توان مشاهده نمود. همانطور که مشاهده می‌کنید، یک کلاینت RavenDB با این بانک اطلاعاتی با پروتکل HTTP و یک REST API ارتباط برقرار می‌کند. برای نمونه، کلاینت در اینجا با اعمال یک HTTP Verb خاص به نام PUT، اطلاعات را درون بانک اطلاعاتی ذخیره کرده است. تبادل اطلاعات نیز با فرمت JSON انجام می‌شود.
عملیات PUT حتما نیاز به یک Id از پیش مشخص دارد و این Id، پیشتر در سطری که Hilo در آن ذکر شده (یکی از الگوریتم‌های محاسبه Id در RavenDB)، محاسبه گردیده است. برای نمونه در اینجا الگوریتم Hilo مقدار "questions/1" را به عنوان Id محاسبه شده بازگشت داده است.
در سطری که عملیات Post به آدرس bulk_docs سرور ارسال گردیده است، کار ارسال یکباره چندین شیء JSON برای کاهش رفت و برگشت‌ها به سرور انجام می‌شود.

و برای کوئری گرفتن مقدماتی از اطلاعات ثبت شده می‌توان نوشت:
 using (var session = store.OpenSession())
{
  var question1 = session.Load<Question>("questions/1");
  Console.WriteLine(question1.By);
}

نگاهی به بانک اطلاعاتی ایجاد شده

در همین حال که سرور RavenDB در حال اجرا است، مرورگر دلخواه خود را گشوده و سپس آدرس http://localhost:8080 را وارد نمائید. بلافاصله، کنسول مدیریتی تحت وب این بانک اطلاعاتی که با سیلورلایت نوشته شده است، ظاهر خواهد شد:


و اگر بر روی هر سطر اطلاعات دوبار کلیک کنید، به معادل JSON آن نیز خواهید رسید:


اینبار برنامه را به صورت زیر تغییر دهید تا روابط بین کلاس‌ها را نیز پیاده سازی کند:
using System;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
                               {
                                   Url = "http://localhost:8080"
                               }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    var question = new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test...."
                    };                 
                    question.Answers.Add(new Answer
                    {
                         By = "users/Farid",
                         Content = "بررسی می‌شود"
                    });
                    session.Store(question);

                    session.SaveChanges();
                }

                using (var session = store.OpenSession())
                {
                    var question1 = session.Load<Question>("questions/1");
                    Console.WriteLine(question1.By);
                }
            }
        }
    }
}
در اینجا یک سؤال به همراه پاسخی به آن تعریف شده است. همچنین در مرحله بعد، نحوه کوئری گرفتن مقدماتی از اطلاعات را بر اساس Id سند مرتبط، مشاهده می‌کنید. چون یک Session، الگوی واحد کار را پیاده سازی می‌کند، اگر پس از Load یک سند، خواصی از آن‌را تغییر دهیم و در پایان Session متد SaveChanges فراخوانی شود، به صورت خودکار این تغییرات به بانک اطلاعاتی نیز اعمال خواهند شد (روش به روز رسانی اطلاعات). این مورد بسیار شبیه است به مباحث پایه ای Change tracking که در بسیاری از ORMهای معروف تاکنون پیاده سازی شده‌اند. روش حذف اطلاعات نیز به همین ترتیب است. ابتدا سند مورد نظر یافت شده و سپس متد session.Delete بر روی این شیء یافت شده فراخوانی گردیده و در پایان سشن باید SaveChanges جهت نهایی شدن تراکنش فراخوانی گردد.

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


نکته جالبی که در اینجا وجود دارد، عدم نیاز به join نویسی برای دریافت اطلاعات وابسته به یک شیء است. اگر سؤالی وجود دارد، پاسخ‌های به آن و یا سایر نظرات، یکجا داخل همان سؤال ذخیره می‌شوند و به این ترتیب سرعت دسترسی نهایی به اطلاعات بیشتر شده و همچنین قفل گذاری روی سایر اسناد کمتر. این مساله نیز به ذات NoSQL و یا غیر رابطه‌ای RavenDB بر می‌گردد. در بانک‌های اطلاعاتی NoSQL، مفاهیمی مانند کلیدهای خارجی، JOIN بین جداول و امثال آن وجود خارجی ندارند. برای نمونه اگر به کلاس‌های مدل‌های برنامه دقت کرده باشید، خبری از وجود Id در آن‌ها نیست. RavenDB یک Document store است و نه یک Relation store. در اینجا کل درخت تو در توی خواص یک شیء دریافت و به صورت یک سند ذخیره می‌شود. به حاصل این نوع عملیات در دنیای بانک‌های اطلاعاتی رابطه‌ای، Denormalized data هم گفته می‌شود.
البته می‌توان به کلاس‌های تعریف شده خاصیت رشته‌ای Id را نیز اضافه کرد. در این حالت برای مثال در حالت فراخوانی متد Load، این خاصیت رشته‌ای، با Id تولید شده توسط RavenDB مانند "questions/1" مقدار دهی می‌شود. اما از این Id برای تعریف ارجاعات به سؤالات و پاسخ‌های متناظر استفاده نخواهد شد؛ چون تمام آن‌ها جزو یک سند بوده و داخل آن قرار می‌گیرند.
نظرات مطالب
پرسش و پاسخ‌های متداول ایجاد یک وبلاگ بلاگری
با سلام وخسته نباشید
دو تا سوال داشتم
سوال اول : توی قالب های جدید بلاگر (قالب هایی با فرمت جدید) ، وقتی روی
Edit HTML
می رم و متن داخل اون رو توی برنامه ی
Microsoft FrontPage
کپی می کنم تا بتونم ویرایشش کنم ، چیزی نشون نمیده و قالب رو به من نشون نمیده . مثلا می خوام واسه ی بک گراند قالبم از عکس استفاده کنم ولی محتویات قالب نشون داده نمیشه . مشکل از کجاست و چطوری باید حلش کنم تا بتونم یه قالب جدید با بک گراند مخصوص درست کنم؟
سوال دوم : توی قالب های کلاسیک بلاگر ، برای تبدیل تاریخ میلادی به شمسی از روشی که توی پست
https://www.dntips.ir/2008/12/blogger.html
توضیح داد هبودین استفاده کردم . ولی دو تا مشکل وجود داره :
اولا اینکه فرمت و رنگ تاریخ متن به صورت مشکی و بزرگ در اومد . چطوری میتونم سایز و رنگ تاریخ رو عوض کنم ؟
دوم اینکه تاریخ آرشیو رو چطور می تونم به فارسی تبدیل کنم؟
وبلاگ آزمایشی من تو این ادرسه که با قالب کلاسیک ، تاریخش رو به شمسی تبدیل کردم :
http://arad-diarist2.blogspot.com
ممنون می شم اگه راهنمایی کنید
مطالب
ارسال خودکار مطلب به بلاگر

اکثر خدمات گوگل دارای API هم هستند و به این ترتیب با استفاده از برنامه نویسی نیز می‌توان به آن‌ها دسترسی پیدا کرد. برای نمونه API دسترسی به Blogger در اینجا توضیح داده شده است. برای کار با این امکانات یا می‌توان چرخ را از نو اختراع کرد یا از کتابخانه‌های مرتبطی همانند Gdata API for .NET استفاده نمود. برای دات نت فریم ورک، از آدرس http://code.google.com/p/google-gdata/ می‌توان آخرین کتابخانه‌های کار با GData یا Google Data API را دریافت کرد. برای نمونه فایل Google_Data_API_Setup_1.9.0.0.msi فعلی آن حدود 28 مگ حجم دارد و به درد کسانی می‌خورد که علاقمند هستند تا تمام امکانات موجود آن‌را بررسی کنند. راه ساده‌تری هم برای دسترسی به این کتابخانه‌ها وجود دارد؛ می‌توان از NuGet استفاده کرد.


به این ترتیب به سادگی و سرعت هرچه تمامتر فایل 200 کیلوبایتی Google.GData.Client.dll دریافت شده و ارجاعی نیز به آن اضافه خواهد شد. همین حد جهت کار با بلاگر کافی است.
برای نمونه قطعه کد زیر کار ارسال یک مطلب جدید به وبلاگ بلاگری شما را انجام خواهد داد:

using System;
using System.Collections.Generic;
using Google.GData.Client;

namespace BloggerAutoPoster
{
public class BloggerAutoPoster
{
public string UserName { set; get; }

public string Password { set; get; }

public string PostTitle { set; get; }

public IList<string> PostTags { set; get; }

public string PostBody { set; get; }

public string BlogUrl { set; get; }

public bool PostAsDraft { set; get; }

public bool PostNewEntry()
{
var service = new Service("blogger", "blogger-example")
{
Credentials = new GDataCredentials(UserName, Password)
};
var newPost = constructNewEntry();
var result = service.Insert(new Uri(BlogUrl), newPost);
return result != null;
}

private AtomEntry constructNewEntry()
{
var newPost = new AtomEntry
{
Title = { Text = PostTitle },
Content = new AtomContent
{
Content = string.Format(@"<div xmlns=""http://www.w3.org/1999/xhtml"">{0}</div>", PostBody),
Type = "xhtml"
},
IsDraft = PostAsDraft
};

foreach (var tag in PostTags)
{
newPost.Categories.Add(
new AtomCategory
{
Term = tag,
Scheme = "http://www.blogger.com/atom/ns#"
});
}

return newPost;
}
}
}

مثالی از استفاده آن هم به صورت زیر می‌باشد:

new BloggerAutoPoster
{
BlogUrl = "https://www.blogger.com/feeds/number/posts/default",
UserName = "name@gmail.com",
Password = "pass",
PostTitle = "بررسی ارسل خودکار-3",
PostTags = new List<string> { "بررسی ارسال خودکار" },
PostBody = "تست می‌شود123",
PostAsDraft = false
}.PostNewEntry();

نام کاربری و کلمه عبور آن، همان مشخصات وارد شدن به اکانت جی‌میل شما است. اگر می‌خواهید مطلب ارسالی بلافاصله در سایت ظاهر نشود PostAsDraft را true کنید. همچنین BlogUrl آن، همانطور که ملاحظه می‌کنید فرمت خاصی دارد. جهت یافتن آن می‌توان از قطعه کد زیر کمک گرفت:

using System;
using System.Collections.Generic;
using System.Linq;
using Google.GData.Client;

namespace BloggerAutoPoster
{
public class BlogInfo
{
public string Title { set; get; }
public string Url { set; get; }
}

public class BloggerInfo
{
public static IList<BlogInfo> FindMyBlogsUrls(string username, string password)
{
var result = new List<BlogInfo>();

var service = new Service("blogger", "blogger-example")
{
Credentials = new GDataCredentials(username, password)
};

var query = new FeedQuery { Uri = new Uri("https://www.blogger.com/feeds/default/blogs") };
var feed = service.Query(query);

if (feed == null)
throw new NotSupportedException("You don't have any blogs!");

foreach (var entry in feed.Entries)
{
result.AddRange(entry.Links.Where(t => t.Rel.Equals("http://schemas.google.com/g/2005#post"))
.Select(t => new BlogInfo
{
Url = new Uri(t.HRef.ToString()).AbsoluteUri,
Title = entry.Title.Text
}));
}

return result;
}
}
}

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


مطالب
بررسی تصویر امنیتی (Captcha) سایت - قسمت دوم

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


// RandomGenerator.cs
using System;
using System.Security.Cryptography;

namespace PersianCaptchaHandler
{
    public class RandomGenerator
    {
        private static readonly byte[] Randb = new byte[4];
        private static readonly RNGCryptoServiceProvider Rand = new RNGCryptoServiceProvider();

        public static int Next()
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int max)
        {
            Rand.GetBytes(Randb);
            var value = BitConverter.ToInt32(Randb, 0);
            value = value%(max + 1);
            if (value < 0) value = -value;
            return value;
        }
        public static int Next(int min, int max)
        {
            var value = Next(max - min) + min;
            return value;
        }
    }
}
و برای تبدیل عدد تصادفی بدست آمده به متن نیز از این کلاس میتوان استفاده کرد که به طرز ساده ای این کار را انجام میدهد:
// NumberToString.cs
namespace PersianCaptchaHandler
{
    public class NumberToString
    {

        #region Fields
        private static readonly string[] Yakan = new[] { "صفر", "یک", "دو", "سه", "چهار", "پنج", "شش", "هفت", "هشت", "نه" };
        private static readonly string[] Dahgan = new[] { "", "", "بیست", "سی", "چهل", "پنجاه", "شصت", "هفتاد", "هشتاد", "نود" };
        private static readonly string[] Dahyek = new [] { "ده", "یازده", "دوازده", "سیزده", "چهارده", "پانزده", "شانزده", "هفده", "هجده", "نوزده" };
        private static readonly string[] Sadgan = new [] { "", "یکصد", "دوصد", "سیصد", "چهارصد", "پانصد", "ششصد", "هفتصد", "هشتصد", "نهصد" };
        private static readonly string[] Basex = new [] { "", "هزار", "میلیون", "میلیارد", "تریلیون" };
        #endregion

        private static string Getnum3(int num3)
        {
            var s = "";
            var d12 = num3 % 100;
            var d3 = num3 / 100;
            if (d3 != 0)
                s = Sadgan[d3] + " و ";
            if ((d12 >= 10) && (d12 <= 19))
            {
                s = s + Dahyek[d12 - 10];
            }
            else
            {
                var d2 = d12 / 10;
                if (d2 != 0)
                    s = s + Dahgan[d2] + " و ";
                var d1 = d12 % 10;
                if (d1 != 0)
                    s = s + Yakan[d1] + " و ";
                s = s.Substring(0, s.Length - 3);
            }
            return s;
        }

        public static string ConvertIntNumberToFarsiAlphabatic(string snum)
        {
            var stotal = "";
            if (snum == "0") return Yakan[0];

            snum = snum.PadLeft(((snum.Length - 1) / 3 + 1) * 3, '0');
            var l = snum.Length / 3 - 1;
            for (var i = 0; i <= l; i++)
            {
                var b = int.Parse(snum.Substring(i * 3, 3));
                if (b != 0)
                    stotal = stotal + Getnum3(b) + " " + Basex[l - i] + " و ";
            }
            stotal = stotal.Substring(0, stotal.Length - 3);
            return stotal;
        }
    }
}
و برای کد کردن و دیکد کردن یعنی موارد سوم و چهارم مقاله قبلی، متن بدست آمده را که بعنوان قسمتی از آدرس تصویر در ادامه آدرس هندلر معرفی شده می‌آید تبدیل به یک string بی معنی برای بازدیدکننده میکند:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

namespace PersianCaptchaHandler
{
    public class Encryptor
    {
        #region constraints
        private static string Password { get { return "Mehdi"; } }
        private static string Salt { get { return "Payervand"; } }
        #endregion

        public static string Encrypt(string clearText)
        {
            // Turn text to bytes
            var clearBytes = Encoding.Unicode.GetBytes(clearText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write);

            cs.Write(clearBytes, 0, clearBytes.Length);
            cs.Close();

            var encryptedData = ms.ToArray();

            return Convert.ToBase64String(encryptedData);
        }
        public static string Decrypt(string cipherText)
        {
            // Convert text to byte
            var cipherBytes = Convert.FromBase64String(cipherText);

            var pdb = new PasswordDeriveBytes(Password, Encoding.Unicode.GetBytes(Salt));

            var ms = new MemoryStream();
            var alg = Rijndael.Create();

            alg.Key = pdb.GetBytes(32);
            alg.IV = pdb.GetBytes(16);

            var cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write);

            cs.Write(cipherBytes, 0, cipherBytes.Length);
            cs.Close();

            var decryptedData = ms.ToArray();

            return Encoding.Unicode.GetString(decryptedData);
        }
    }
}


و نیز برای اعتبار سنجی عدد دریافتی از کاربر میتوان از عبارت با قاعده زیر استفاده کرد:

// Utils.cs
using System.Text.RegularExpressions;

namespace PersianCaptchaHandler
{
    public class Utils
    {
        private static readonly Regex NumberMatch = new Regex(@"^([0-9]*|\d*\.\d{1}?\d*)$", RegexOptions.Compiled);
        public static bool IsNumber(string number2Match)
        {
            return NumberMatch.IsMatch(number2Match);
        }
    }
}
برای استفاده نیز کافیست که هندلر مربوط به این کتابخانه را بطریق زیر در وب کانفیگ رجیستر کرد:

<add verb="GET" path="/captcha/" type="PersianCaptchaHandler.CaptchaHandler, PersianCaptchaHandler, Version=1.0.0.0, Culture=neutral"  />
و در صفحه مورد نظرتان بطریق زیر میتوان از یک تگ تصویر برای نمایش تصویر تولیدی و از یک فیلد مخفی برای نگهداری مقدار کد شده معادل عدد تولیدی که در هنگام مقایسه با عدد ورودی کاربر مورد نیاز است استفاده شود:

<!-- ASPX -->
<dl>
    <dt>تصویر امنیتی</dt>
    <dd>
        <asp:Image ID="imgCaptchaText" runat="server" AlternateText="CaptchaImage" />
        <asp:HiddenField ID="hfCaptchaText" runat="server" />
        <asp:ImageButton ID="btnRefreshCaptcha" runat="server" ImageUrl="/img/refresh.png"
            OnClick="btnRefreshCaptcha_Click" />
        <br />
        <asp:TextBox ID="txtCaptcha" runat="server" AutoCompleteType="Disabled"></asp:TextBox>
    </dd>
    <dd>
        <asp:Button ID="btnSubmit" runat="server" Text="ثبت" OnClick="btnSubmit_Click" />
    </dd>
</dl>
<asp:Label ID="lblMessage" runat="server"></asp:Label>
در زمان لود صفحه، تصویر امنیتی مقدار دهی میشود و در زمان ورود عدد توسط کاربر با توجه به اینکه کاربر حتما باید عدد وارد کند با عبارت با قاعده این اعتبار سنجی انجام میشود:

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
                SetCaptcha();
        }

        private void SetCaptcha()
        {
            lblMessage.Text =
            txtCaptcha.Text = string.Empty;

            var newNumber =
                RandomGenerator.Next(100, 999)
                ;

            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(newNumber.ToString());

            hfCaptchaText.Value =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            txtCaptcha.Text = string.Empty;
            imgCaptchaText.ImageUrl =
                "/captcha/?text=" + hfCaptchaText.Value;
        }
و بعد از ورود عدد از سمت کاربر از متد تبدیل به حروف استفاده کرده و این مقدار تولیدی با مقدار فیلد مخفی مقایسه میشود:
        private string GetCaptcha()
        {
            var farsiAlphabatic = NumberToString.ConvertIntNumberToFarsiAlphabatic(txtCaptcha.Text);

            var encryptedString =
                HttpUtility
                .UrlEncode(
                    Encryptor.Encrypt(
                        farsiAlphabatic
                    )
                );

            return encryptedString;
        }

        private bool ValidateUserInputForLogin()
        {
            if (!Utils.IsNumber(txtCaptcha.Text))
            {
                lblMessage.Text = "تصویر امنیتی را بطور صحیح وارد نکرده اید";
                return false;
            }

            var strGetCaptcha =
                GetCaptcha();

            var strDecodedVAlue =
                hfCaptchaText.Value;

            if (strDecodedVAlue != strGetCaptcha)
            {
                lblMessage.Text = "کلمه امنیتی اشتباه است";
                SetCaptcha();
                return false;
            }
            return true;
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            if (!ValidateUserInputForLogin()) return;
            lblMessage.Text = "کلمه امنیتی درست است";
        }

        protected void btnRefreshCaptcha_Click(object sender, ImageClickEventArgs e)
        {
            SetCaptcha();
        }
 در آخر این پروژه در کدپلکس قرار داده شده، و مشتاق نظرات و پیشنهادات شما هستم و نیز نمونه مثال بالا ضمیمه شده است
پروژه‌ها
DNTViewer
برنامه سورس باز DNTViewer، مشاهده‌گر آفلاین اطلاعات فایل دیتابیس سایت جاری است که با فرمت XML از آدرس ذیل قابل دریافت می‌باشد:



برای استفاده از آن نیاز به نصب دات نت فریم ورک 4 بر روی سیستم شما است. همچنین فایل XML بانک اطلاعاتی سایت را پس از دریافت نیاز است در کنار فایل EXE برنامه کپی کنید.