نظرات مطالب
مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
سلام

راستش من منوهای چند سطحی پویا را برای bootstrap navbar نوشتم. الگوریتمش را مجبور شدم به صورت بازگشتی بنویسم.اگر کسی می‌تونه به صورت غیر بازگشتی بگه ممنونش میشم.
این کد یه partialpage برای navbar هست که هرکسی برای bootstrap به راحتی میتونه استفاده کنه.

@model IEnumerable<DomainClasses.Page>
@helper ShowNavBar(IEnumerable<DomainClasses.Page> pages)
{
    
    foreach (var page in pages)
    {
        if (page != null)
        {
            if (page.Children.Count == 0)
            {
                <text><li><a tabindex="-1" href="#">@page.Title</a></li></text>
            }

            if (page.Children.Count > 0 && page.Parent == null)
            {         
                <text><li class="dropdown"><a class="dropdown-toggle" id="dLabel" role="button" data-toggle="dropdown" data-target="#" href="/page.html">@page.Title<b class="caret"></b></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li><a tabindex="-1" href="#">@page.Title</a></li></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

            if (page.Children.Count > 0 && page.Parent != null)
            {         
                <text><li class="dropdown-submenu"><a tabindex="-1" href="#">@page.Title</a><ul class="dropdown-menu"></text>
                @ShowNavBar(page.Children)
                @:</ul></li>
            }

        }
    }
    
}
<div class="navbar" style="margin-bottom: 10px;">
    <div class="navbar-inner">
        <a class="brand" href="www.google.com">IT-EBOOK</a>
        <ul class="nav">
            <li class="active"><a href="#">خانه</a></li>
            <li><a href="#">ورود</a></li>
            @ShowNavBar(Model)
            <li><a href="#">ارتباط با ما</a></li>
        </ul>
        <div class="input-append pull-left visible-desktop" style="margin-top: 5px;">
            <input class="span6 search-input" id="Text1" type="text">
            <button class="btn btn-primary" type="button">جست و جو</button>
            <button class="btn btn-info btn-advanced-search" type="button">پیشرفته</button>
        </div>
    </div>
</div>



الان تنها مشکلم اینه که فیلد‌های اضافی هم کوئری گرفته میشه.میدونم فیلدهای اضافی(بر اساس مدلی که ذکر کردم) را چگونه با استفاده از select حذف کنم اما توی viewmodel نمیدونم چه جوری children را از اطلاعات پر کنم؟
 ممنون


 
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging
ASP.NET Core به همراه یک فریم ورک توکار ثبت وقایع (Logging) ارائه شده‌ی توسط تزریق وابستگی‌ها است که به صورت پیش فرض نیز فعال است.


این تصویر را پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 6 - سرویس‌ها و تزریق وابستگی‌ها» مشاهده کرده‌اید. در اینجا لیست سرویس‌هایی را مشاهده می‌کنید که به صورت پیش فرض، ثبت شده‌اند و فعال هستند و ILogger و ILoggerFactory نیز جزئی از آن‌ها هستند. بنابراین نیازی به فعال سازی آن‌ها نیست؛ اما برای استفاده‌ی از آن‌ها نیاز به انجام یک سری تنظیمات است.


پیاده سازی ثبت وقایع در ASP.NET Core

اولین قدم کار با فریم ورک ثبت وقایع ASP.NET Core، معرفی ILoggerFactory به متد Configure کلاس آغازین برنامه است:
public void Configure(ILoggerFactory loggerFactory, IApplicationBuilder app, IHostingEnvironment env)
{
   loggerFactory.AddConsole(Configuration.GetSection("Logging"));
   loggerFactory.AddDebug();
متد Configure امضای مشخصی را ندارد و در اینجا به هر تعداد سرویسی که نیاز باشد، می‌توان اینترفیس‌های آن‌ها را جهت تزریق وابستگی‌های متناظر توسط IoC Containser توکار ASP.NET Core، معرفی کرد. در اینجا برای تنظیم ویژگی‌های سرویس ثبت وقایع، تزریق وابستگی ILoggerFactory  صورت گرفته‌است.
سطر اول متد، تنظیمات ثبت وقایع را از خاصیت Logging فایل appsettings.json برنامه می‌خواند (در مورد خاصیت Configuration، در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» بیشتر بحث شد) و لاگ کردن ویژه‌ی در کنسول NET Core. را فعال می‌کند:
{
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}
در مورد Log Level و یا سطوح ثبت وقایع، در ادامه‌ی مطلب بحث خواهد شد.

و سطر دوم سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو می‌شود.
متد AddDebug برای شناسایی، نیاز به افزودن وابستگی‌های ذیل در فایل project.json برنامه را دارد:
{
    "dependencies": {
        //same as before 
        "Microsoft.Extensions.Logging": "1.0.0",
        "Microsoft.Extensions.Logging.Console": "1.0.0",
        "Microsoft.Extensions.Logging.Debug": "1.0.0" 
    }
}
پس از این تنظیمات، برنامه را اجرا کنید.


در اینجا می‌توانید ریز وقایعی را که توسط ASP.NET Core لاگ شده‌است، مشاهده کنید. برای مثال چه درخواستی صورت گرفته‌است و چقدر اجرای آن زمان‌برده‌است.
این فعال سازی مرتبط است به متد AddDebug که اضافه شد. اگر می‌خواهید خروجی AddConsole را هم مشاهده کنید، از طریق خط فرمان، به پوشه‌ی اصلی پروژه وارد شده و سپس دستور dotnet run را اجرا کنید:


دستور dotnet run سبب راه اندازی وب سرور برنامه بر روی پورت 5000 شده‌است که در تصویر نیز مشخص است.
بنابراین اینبار برای دسترسی به برنامه باید مسیر http://localhost:5000 را در مرورگر خود طی کنید. در اینجا نیز می‌توان حالت‌های مختلف اطلاعات لاگ شده را مشاهده کرد و تمام این‌ها مرتبط هستند به ذکر متد AddConsole .


کار با سرویس ثبت وقایع ASP.NET Core از طریق تزریق وابستگی‌ها

برای کار با سرویس ثبت وقایع توکار ASP.NET Core در قسمت‌های مختلف برنامه، می‌توان از ترزیق وابستگی ILogger آن استفاده کرد:
[Route("[controller]")]
public class AboutController : Controller
{
    private readonly ILogger<AboutController> _logger;
 
    public AboutController(ILogger<AboutController> logger)
    {
        _logger = logger;
    }
 
    [Route("")]
    public ActionResult Hello()
    {
        _logger.LogInformation("Running Hello");
        return Content("Hello from DNT!");
    }
در این کنترلر، وابستگی اینترفیس ILogger با پارامتری از نوع کنترلر جاری به سازنده‌ی کلاس تزریق شده‌است. علت ذکر این پارامتر جنریک این است که ILoggerFactory بداند چگونه باید متد CreateLogger خود را در پشت صحنه وهله سازی کند.
سپس با توجه به اینکه این سرویس جزو سرویس‌های از پیش ثبت شده‌ی ASP.NET Core است، امکانات آن بدون نیاز به تنظیمات بیشتری در دسترس است. برای مثال از متد LogInformation آن در اکشن متد Hello استفاده شده‌است و خروجی عبارت لاگ شده‌ی آن‌را در اینجا می‌توانید مشاهده کنید:



سطوح مختلف ثبت وقایع

اینترفیس ILogger به همراه متدهای مختلفی است؛ مانند LogError، LogDebug و غیره. معانی آن‌ها به شرح زیر هستند:
Debug (1): ثبت واقعه‌ای است با بیشترین حد جزئیات ممکن که عموما شامل اطلاعات حساسی نیز می‌باشد. بنابراین نباید در حالت ارائه‌ی نهایی برنامه فعال شود.
(2) Verbose: ثبت وقایعی مفصل، جهت بررسی مشکلات در حین توسعه‌ی برنامه. تنها باید حاوی اطلاعاتی برای دیباگ برنامه باشند.
(3) Information: عموما برای ردیابی قسمت‌های مختلف برنامه مورد استفاده قرار می‌گیرند.
(4) Warning: جهت ثبت واقعه‌ای نامطلوب در سیستم بکار می‌رود و سبب قطع اجرای برنامه نمی‌شود.
(5) Errors: مشکلات برنامه را که سبب قطع سرویس دهی آن شده‌اند را ثبت می‌کند. هدف آن ثبت مشکلات واحد جاری است و نه کل برنامه.
Critical (6): هدف آن ثبت مشکلات بحرانی کل سیستم است که سبب از کار افتادن آن شده‌اند.

برای مثال در حین تنظیم متد AddDebug که سبب نمایش اطلاعات لاگ شده در کنسول دیباگ ویژوال استودیو می‌شود، می‌توان حداقل سطح ثبت وقایع را نیز ذکر کرد:
 loggerFactory.AddDebug(minLevel: LogLevel.Information);
این حداقل مرتبط است با اعدادی که در کنار سطوح فوق ملاحظه می‌کنید. برای مثال اگر حداقل سطح ثبت وقایع به Information تنظیم شود، چون سطح آن 3 است، دیگر سطوح پایین‌تر از آن لاگ نخواهند شد. اهمیت این مساله در اینجا است که اگر صرفا نیاز به اطلاعات Critical داشتیم، نیازی نیست تا با انبوهی از اطلاعات لاگ شده سر و کار داشته باشیم و به این ترتیب می‌توان حجم اطلاعات نمایش داده شده را کاهش داد.

البته ترتیب واقعی این سطوح را در enum مرتبط با آن‌ها بهتر می‌توان مشاهده کرد:
  public enum LogLevel
  {
    Trace,
    Debug,
    Information,
    Warning,
    Error,
    Critical,
    None,
  }

یک نکته: زمانیکه متد AddDebug را بدون پارامتر فراخوانی می‌کنید، حداقل سطح ثبت وقایع آن به Information تنظیم شده‌است. یعنی در این لاگ، خبری از اطلاعات Debug نخواهد بود (چون سطح دیباگ پایین‌تر است از Information).  بنابراین اگر می‌خواهید این اطلاعات را هم مشاهده کنید باید پارامتر minLevel آن‌را به LogLevel.Debug تنظیم نمائید.


امکان استفاده‌ی از پروایدرهای ثبت وقایع ثالث

تا اینجا، دو نمونه از پروایدرهای توکار ثبت وقایع ASP.NET Core را بررسی کردیم. اگر نیاز به ثبت این اطلاعات با فرمت‌های مختلف و یا در بانک اطلاعاتی وجود دارد، می‌توان به تامین کننده‌های ثالثی که قابلیت کار با ILoggerFactory را دارند نیز مراجعه کرد. برای مثال:
- elmah.io - provider for the elmah.io service
- Loggr - provider for the Loggr service
- NLog - provider for the NLog library
- Serilog - provider for the Serilog library
مطالب
معرفی Microsoft.Data.dll یا WebMatrix.Data.dll

مایکروسافت اخیرا علاوه بر تکمیل ORM های خود مانند LINQ to SQL و همچنین Entity framework ، لایه دیگری را نیز بر روی ADO.NET جهت کسانی که به هر دلیلی دوست ندارند با ORMs کار کنند و از نوشتن کوئری‌های مستقیم SQL لذت می‌برند،‌ ارائه داده است که Microsoft.Data library نام دارد و از قابلیت‌های جدید زبان سی شارپ مانند واژه‌ کلیدی dynamic استفاده می‌کند.

در ادامه قصد داریم جهت بررسی توانایی‌های این کتابخانه از بانک اطلاعاتی معروف Northwind استفاده کنیم. این بانک اطلاعاتی را از اینجا می‌توانید دریافت کنید.

مراحل استفاده از Microsoft.Data library:
الف) این اسمبلی جدید به همراه پروژه WebMatrix ارائه شده است. بنابراین ابتدا باید آن‌را دریافت کنید: +
لازم به ذکر است که این کتابخانه اخیرا به WebMatrix.Data.dll تغییر نام یافته است. (اگر وب را جستجو کنید فقط به Microsoft.Data.dll اشاره شده است)

ب) پس از نصب، ارجاعی را از اسمبلی WebMatrix.Data.dll به پروژه خود اضافه نمائید. این اسمبلی در صفحه‌ی Add References ظاهر نمی‌شود و باید کامپیوتر خود را برای یافتن آن جستجو کنید که عموما در آدرس زیر قرار دارد:
C:\Program Files\Microsoft ASP.NET\ASP.NET Web Pages\v1.0\Assemblies\WebMatrix.Data.dll

ج) اتصال به بانک اطلاعاتی
پیش فرض اصلی این کتابخانه بانک اطلاعاتی SQL Server CE است. بنابراین اگر قصد استفاده از پروایدرهای دیگری را دارید باید به صورت صریح آن‌را ذکر نمائید:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="systemData:defaultProvider" value="System.Data.SqlClient" />
</appSettings>
<connectionStrings>
<add name="Northwind"
connectionString="Data Source=(local);Integrated Security = true;Initial Catalog=Northwind"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>

این تعاریف در فایل web.config و یا app.config برنامه وب یا ویندوزی شما قرار خواهند گرفت.

د) نحوه‌ی تعریف کوئری‌ها و دریافت اطلاعات
using System;
using WebMatrix.Data;

namespace TestMicrosoftDataLibrary
{
class Program
{
static void Main(string[] args)
{
getProducts();

Console.Read();
Console.WriteLine("Press a key ...");
}

private static void getProducts()
{
using (var db = Database.Open("Northwind"))
{
foreach (var product in db.Query("select * from products where UnitsInStock < @0", 20))
{
Console.WriteLine(product.ProductName + " " + product.UnitsInStock);
}
}
}
}
}
پس از افزودن ارجاعی به اسمبلی WebMatrix.Data و مشخص سازی رشته‌ی اتصالی به بانک اطلاعاتی، استفاده از آن جهت دریافت اطلاعات کوئری‌ها همانند چند سطر ساده‌ی فوق خواهد بود که از امکانات dynamic زبان سی شارپ 4 استفاده می‌کند؛ به این معنا که product.ProductName و product.UnitsInStock در زمان اجرا مورد ارزیابی قرار خواهند گرفت.
همچنین نکته‌ی مهم دیگر آن نحوه‌ی تعریف پارامتر در آن است (همان 0@ ذکر شده) که نسبت به ADO.NET کلاسیک به شدت ساده شده‌است (و نوشتن کوئری‌های امن و SQL Injection safe را تسهیل می‌کند).
در اینجا Database.Open کار گشودن name ذکر شده در فایل کانفیگ برنامه را انجام خواهد داد. اگر بخواهید این تعاریف را در کدهای خود قرار دهید (که اصلا توصیه نمی‌شود)، می‌توان از متد Database.OpenConnectionString استفاده نمود.

یا مثالی دیگر: استفاده از LINQ حین تعریف کوئری‌ها:
private static void getCustomerFax()
{
using (var db = Database.Open("Northwind"))
{
var product = db.Query("SELECT * FROM [Customers] WHERE City=@0", "Paris").FirstOrDefault();
if (product != null)
Console.WriteLine(product.Fax);
else
Console.WriteLine("not found.");
}
}

ه) اجرای کوئری‌ها بر روی بانک اطلاعاتی
private static void ExecQuery()
{
using (var db = Database.Open("Northwind"))
{
int affectedRecords = db.Execute("UPDATE [Customers] SET fax = fax + '*' WHERE City = @0", "Paris");
Console.WriteLine("Affected records: {0}", affectedRecords);
}
}

با استفاده از متد Execute آن می‌توان کوئری‌های دلخواه خود را بر روی بانک اطلاعاتی اجرا کرد. خروجی آن تعداد رکورد تغییر کرده است.

و) نحوه‌ی اجرای یک رویه ذخیره شده و نمایش خروجی آن
private static void ExecSPShowResult()
{
using (var db = Database.Open("Northwind"))
{
var customer = db.Query("exec CustOrderHist @0", "ALFKI").FirstOrDefault();
if (customer != null)
{
Console.WriteLine(customer.ProductName);
}
}
}
در این مثال رویه ذخیره شده CustOrderHist در بانک اطلاعاتی Northwind اجرا گردیده و سپس اولین خروجی آن نمایش داده شده است.

ز) اجرای یک تابع و نمایش خروجی آن
private static void useFuncs()
{
using (var db = Database.Open("Northwind"))
{
var query = db.Query("SELECT dbo.FN_GET_CATEGORY_TREE(@0) as Rec1", 3);
foreach(var tree in query)
{
Console.WriteLine(tree.Rec1);
}
}
}
در اینجا تابع FN_GET_CATEGORY_TREE موجود در بانک اطلاعاتی Northwind انتخاب گردیده و سپس خروجی آن به کمک یک نام مستعار (برای مثال Rec1) نمایش داده شده است.

سؤال : آیا WebMatrix.Data.dll بهتر است یا استفاده از ORMs ؟

در اینجا چون از قابلیت‌های داینامیک زبان سی شارپ 4 استفاده می‌شود، کامپایلر درکی از اشیاء خروجی و خواص آن‌ها برای مثال tree.Rec1 (در مثال آخر) ندارد و تنها در زمان اجرا است که مشخص می‌شود آیا یک چنین ستونی در خروجی کوئری وجود داشته است یا خیر. اما حین استفاده از ORMs این طور نیست و Schema یک بانک اطلاعاتی پیشتر از طریق نگاشت‌های جداول به اشیاء دات نتی، به کامپایلر معرفی می‌شوند و همین امر سبب می‌شود تا اگر ساختار بانک اطلاعاتی تغییر کرد، پیش از اجرای برنامه و در حین کامپایل بتوان مشکلات را دقیقا مشاهده نمود و سپس برطرف کرد.
ولی در کل استفاده از این کتابخانه نسبت به ADO.NET کلاسیک بسیار ساده‌تر بوده، می‌توان اشیاء و خواص آن‌ها را مطابق نام جداول و فیلدهای بانک اطلاعاتی تعریف کرد و همچنین تعریف پارامترها و برنامه نویسی امن نیز در آن بسیار ساده‌تر شده است.

برای مطالعه بیشتر:
Introduction to Microsoft.Data.dll

مطالب
بررسی شیوه‌نامه‌های المان‌های پر کاربرد بوت استرپ 4
در این قسمت، شیوه‌نامه‌هایی را بررسی می‌کنیم که به المان‌های پر کاربردی مانند دکمه‌ها، لیست‌ها و نشان‌ها اعمال می‌شوند.


شیوه‌نامه‌های کار با دکمه‌ها در بوت استرپ 4

کلاس پایه ایجاد دکمه‌های بوت استرپی، کلاس btn است. البته آن‌را می‌توان با کلاس‌های دیگری نیز ترکیب کرد:
- کلاس btn را می‌توان بر روی المان‌هایی مانند anchor، button و input نیز اعمال کرد:
<a class="btn btn-primary" href="#" role="button">Link</a>
<button class="btn btn-primary" type="submit">Button</button>
<input class="btn btn-primary" type="submit" value="Input">
در این حالت تمام این المان‌ها یکسان به نظر می‌رسند:


- برای تعیین رنگ اجباری دکمه‌های بوت استرپ، از فرمول زیر استفاده می‌شود؛ مانند btn-primary:

<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-danger">Danger</button>
<button class="btn btn-warning">Warning</button>
<button class="btn btn-info">Info</button>
<button class="btn btn-light">Light</button>
<button class="btn btn-dark">Dark</button>
با این خروجی:


همانطور که ملاحظه می‌کنید، رنگ این دکمه‌ها نیز نسبت به نگارش سوم آن، به روز رسانی شده‌است.

همچنین نگارش outline آن‌ها نیز قابل تعریف است؛ مانند btn-outline-primary:

<button class="btn btn-outline-primary">Primary</button>
<button class="btn btn-outline-secondary">Secondary</button>
<button class="btn btn-outline-success">Success</button>
<button class="btn btn-outline-danger">Danger</button>
<button class="btn btn-outline-warning">Warning</button>
<button class="btn btn-outline-info">Info</button>
<button class="btn btn-outline-light">Light</button>
<button class="btn btn-outline-dark">Dark</button>
با این خروجی:


- کلاس btn-size که در اینجا size می‌تواند sm یا lg باشد و سبب ایجاد دکمه‌هایی کوچک یا بزرگ می‌شوند.
- کلاس btn-block سبب می‌شود تا دکمه‌ای کل عرض container را پر کند.
<button class="btn btn-primary">Default</button>
<button class="btn btn-primary btn-lg">Large</button>
<button class="btn btn-primary btn-sm">Small</button>
<button class="btn btn-primary btn-block">Block</button>
با این خروجی:


- امکان اعمال کلاس‌های active و disabled نیز در اینجا میسر است:
<h2>States</h2>
<h3>Active</h3>
<button class="btn btn-primary active">Active Button</button>

<h3>Disabled</h3>
<button class="btn btn-primary disabled">Disabled Button</button>
<a class="btn btn-primary disabled" href="#">Disabled Link Button</a>
با این خروجی:



تشکیل گروهی از دکمه‌ها در بوت استرپ 4

برای تبدیل تعدادی از دکمه‌ها به یک گروه، از کلاس btn-group استفاده می‌شود. همچنین امکان تشکیل گزینه‌ی عمودی آن، با کلاس btn-group-vertical نیز وجود دارد. در این  حالت دکمه‌ها بجای نمایش افقی، به صورت یک ستون نمایش داده می‌شوند. کلاس btn-toolbar نیز برای تشکیل نوار ابزاری از دکمه‌ها در نظر گرفته شده‌است. توسط کلاس‌های btn-group-sm و یا btn-group-lg می‌توان اندازه‌ی این گروه‌ها را تغییر داد.
<div class="btn-toolbar" aria-label="All pets">
    <div class="btn-group mb-2 mr-2" aria-label="Common pets">
        <button type="button" class="btn btn-primary active">Cat</button>
        <button type="button" class="btn btn-primary">Dog</button>
        <button type="button" class="btn btn-primary">Fish</button>
        <button type="button" class="btn btn-primary">Bird</button>
    </div>

    <div class="btn-group mb-2" aria-label="Exotic pets">
        <button type="button" class="btn btn-primary">Amphibian</button>
        <button type="button" class="btn btn-primary active">Reptile</button>
        <button type="button" class="btn btn-primary">Other</button>
    </div>
</div>
با این خروجی:


در اینجا دو گروه از دکمه‌ها تشکیل شده‌اند که این‌ها را داخل یک btn-toolbar قرار داده‌ایم. همچنین تعریف aria-labelها به screen readers و معلولین کمک می‌کند.
به علاوه با استفاده از کلاس‌های mb-2 و mr-2، سبب ایجاد margin بین این نوار ابزار با متن زیر آن و همچنین بین خود آن‌ها شده‌ایم.

و حالت عمودی آن:
<div class="btn-group-vertical mb-2" aria-label="Exotic pets">
    <button type="button" class="btn btn-primary">Amphibian</button>
    <button type="button" class="btn btn-primary active">Reptile</button>
    <button type="button" class="btn btn-primary">Other</button>
</div>
چنین شکلی را پیدا می‌کند:



کلاس‌های جدید تشکیل Badges در بوت استرپ 4

برای تشکیل نشان/آرم از کلاس badge استفاده می‌شود و برای تغییر شکل آن می‌توان از کلاس badge-pill کمک گرفت. برای تغییر رنگ آن نیز فرمول زیر وجود دارد:


یک مثال:
    <div class="container">
        <div class="row">
            <section class="col-12">
                <h1>Our Commitment <span class="badge badge-info">to you</span></h1>
                <h3>Grooming <span class="badge badge-danger badge-pill">new</span></h3>
            </section>
        </div><!-- row -->
    </div><!-- content container -->
با این خروجی:


همانطور که مشاهده می‌کنید، یک badge همواره به اندازه‌ی container آن در آمده و نمایش داده می‌شود.


ایجاد لیستی از آیتم‌ها در بوت استرپ 4

بوت استرپ، کلاس‌هایی را برای ایجاد لیست‌هایی با ظاهر لیست‌های اندرویدی به همراه دارد که در ادامه با مثال‌هایی آن‌ها را بررسی خواهیم کرد.
<ul class="list-group mb-3">
    <li class="list-group-item active">Grooming</li>
    <li class="list-group-item list-group-item-action">
        General Health
    </li>
    <li class="list-group-item list-group-item-action">Nutrition</li>
    <li class="list-group-item list-group-item-action">
        Pest Control
    </li>
    <li class="list-group-item list-group-item-action">Vaccinations</li>
</ul>
با این خروجی:

در اینجا برای تشکیل لیستی با ظاهری شکیل‌تر، می‌توان ابتدا کلاس list-group را به ul انتساب داد و سپس به هر کدام از liهای آن کلاس list-group-item انتساب داده می‌شود. با افزودن کلاس active به اولین مورد، ظاهر آن با رنگی متمایز نمایان می‌شود. همچنین اگر علاقمند بودیم تا هر کدام از آیتم‌ها با عبور ماوس بر روی آن‌ها، با رنگ ملایمی مشخص شوند، می‌توان از کلاس list-group-item-action استفاده کرد.

این list-group را بر روی لینک‌ها و دکمه‌ها نیز می‌توان همانند قبل اعمال کرد:
<div class="list-group mb-3">
    <a class="list-group-item active" href="#">Grooming</a>
    <a class="list-group-item list-group-item-action list-group-item-success"
        href="#">Nutrition</a>
    <a class="list-group-item list-group-item-action  list-group-item-info"
        href="#">
        Pest Control
    </a>
    <a class="list-group-item list-group-item-action  list-group-item-warning"
        href="#">Vaccinations</a>
</div>
با این خروجی:


در اینجا از کلاس‌هایی مانند list-group-item-warning برای تغییر رنگ پس زمینه‌ی هر آیتم می‌توان کمک گرفت.

برای تعریف badge برای هر آیتم، می‌توان به صورت زیر عمل کرد:
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
    Nutrition
    <span class="badge badge-info badge-pill">12</span>
</li>
با این خروجی:


با اعمال کلاس‌های badge، این نشان نمایش داده می‌شود؛ اما دقیقا در کنار متن Nutrition در اینجا. برای اینکه آن‌را به سمت دیگر این ردیف منتقل و همچنین تراز عمودی آن‌را نیز به میانه‌ی صفحه تنظیم کنیم، می‌توان از Flexbox کمک گرفت. با اعمال d-flex، این ردیف تبدیل به یک Flexbox container می‌شود و سپس می‌توان کلاس‌های مخصوص Flexbox مانند justify-content-between و align-items-center را به این ردیف اعمال کرد تا ترازهای عمودی و افقی آیتم‌های قرار گرفته‌ی درون آن تغییر کنند.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_08.zip
مطالب
کامپوننت‌های راهبری سایت در بوت استرپ 4
پس از سیستم طرحبندی بوت استرپ، مهم‌ترین کامپوننت‌های آن، کامپوننت‌های راهبری سایت مانند Navs ،Tabs ،Pills و Navbars هستند. Navbars در بوت استرپ 4 نسبت به نگارش سوم آن بازنویسی کامل شده‌اند و شامل بهبودهای قابل ملاحظه‌ای هستند.


کامپوننت‌های Nav در بوت استرپ 4

کامپوننت‌های گروه Nav‌، در نگارش 4 آن به علت استفاده‌ی از Flexbox، تغییرات بسیاری داشته‌اند و در نتیجه‌ی آن، انعطاف پذیرتر و ساده‌تر شده‌اند.
در ابتدا لیست ساده‌ی زیر را در نظر بگیرید. تنظیمات ابتدایی آن برای تبدیل به منوی راهبری بالای سایت به صورت زیر است:
<body>
    <div class="container">
        <div class="row">
            <section class="col-12">
                <ul class="nav">
                    <li class="nav-item"><a class="nav-link active" href="#">Home</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Mission</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Services</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Staff</a></li>
                    <li class="nav-item"><a class="nav-link disabled" href="#">Testimonials</a></li>
                </ul>
</div>
    </div>
</body>
ابتدا کلاس nav به یک ul اضافه می‌شود. سپس به هر آیتم آن، کلاس nav-item را اضافه می‌کنیم. در آخر به هر لینک آن نیز کلاس nav-link نسبت داده می‌شود:


در اینجا دو کلاس active و disabled نیز به لینک‌های منوی راهبری اضافه شده‌اند. البته این کلاس‌ها تا تکمیل نهایی nav‌، ظاهر آنچنان متفاوتی را ارائه نمی‌دهند.
اولین شیوه‌نامه‌ای را که می‌توان به nav اضافه کرد، nav-pills است:
<ul class="nav nav-pills">


Pills شبیه به دکمه‌ها هستند و در این حالت لینک active، واضح‌تر به نظر می‌رسد.
و یا می‌توان nav-tabs را به nav افزود:
<ul class="nav nav-tabs">


روش دیگر تعریف nav، استفاده از المان nav و سپس حذف ul و li و همچنین nav-item است:
<nav class="nav nav-pills justify-content-center">
    <a class="nav-link active" href="#">Home</a>
    <a class="nav-link" href="#">Mission</a>
    <a class="nav-link" href="#">Services</a>
    <a class="nav-link" href="#">Staff</a>
    <a class="nav-link disabled" href="#">Testimonials</a>
</nav>
در اینجا امکان کار با کلاس‌های Flexbox، مانند justify-content-center را نیز مشاهده می‌کنید. برای مثال برای هدایت منوی راهبری به سمت چپ و یا راست صفحه می‌توان بجای center، از end و یا start استفاده کرد. انجام یک چنین کارهایی در نگارش‌های قبلی بوت استرپ بدون Flexbox، مشکل بودند.
کلاس دیگری را که در اینجا می‌توان استفاده کرد، flex-column است تا آیتم‌های nav، بجای نمایش در یک ردیف، در یک ستون ظاهر شوند:


و یا می‌توان با استفاده از break-points، سبب شد تا اگر اندازه‌ی صفحه بیش از sm بود، آیتم‌های منوی راهبری، ردیفی و اگر کمتر از آن بود (حالت موبایل)، ستونی نمایش داده شوند:
<nav class="nav nav-pills justify-content-center flex-column flex-sm-row">


ایجاد navbars در بوت استرپ 4

Navbar بوت استرپ 4، بازنویسی کامل شده و کار کردن با آن نسبت به نگارش سوم آن بسیار ساده‌تر شده‌است.
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
        </div>
    </nav>

    <div class="container">
با این خروجی:


در اینجا، کار با افزودن کلاس navbar به المان nav شروع می‌شود. سپس هر لینک داخل آن، کلاس‌های nav-item nav-link را پیدا می‌کند. در اینجا اگر آیتمی قرار است به صفحه‌ی جاری اشاره کند، با کلاس active مشخص خواهد شد.
سپس توسط  کلاس‌های bg-dark navbar-dark، رنگ‌های پس زمینه و رنگ متن مشخص شده‌اند. برای مثال می‌توان bg-light navbar-light را نیز آزمایش کرد:
<nav class="navbar bg-light navbar-light navbar-expand-sm">


و یا بجای این رنگ‌های پیش‌فرض، در بوت استرپ 4 می‌توان به سادگی رنگ navbar را توسط یک background-color دلخواه، سفارشی سازی کرد:
<nav class="navbar navbar-dark navbar-expand-sm" style="background-color:red">


کاری که در نگارش‌های پیشین بوت استرپ به سادگی میسر نبود.

همچنین اگر دقت کرده باشید از کلاس navbar-expand-sm نیز استفاده شده‌است. حالت پیش‌فرض نمایش آیتم‌های navbar، ستونی است و برای حالت موبایل درنظر گرفته شده‌است. استفاده‌ی از navbar-expand-sm سبب می‌شود تا پس از عرض sm، آیتم‌های navbar همانند شکل‌های فوق، در طی یک ردیف نمایش داده شوند و در عرض کمتر از sm، به صورت یک ستون:


به علاوه آیتم‌های navbar را داخل یک container قرار داده‌ایم:
<div class="container">
      <div class="navbar-nav">
علت اینجا است که چون navbar تعریف شده خارج از container اصلی است، اگر چنین کاری را انجام ندهیم، آیتم‌های آن از سمت چپ صفحه بدون تراز بودن با container ذیل آن نمایش داده خواهند شد. تعریف یک container داخل navbar، این مشکل عدم تراز بودن عمودی را برطرف می‌کند.


تعریف متون و لوگو در navbar بوت استرپ 4

برای تعریف متن لوگوی سایت در navbar به صورت زیر عمل می‌شود:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-brand">
                Wisdom Pet Medicine
            </div>
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
        </div>
    </nav>

    <div class="container">
در اینجا با استفاده از کلاس navbar-brand در یک div مجزا، سبب نمایش متن لوگوی سایت شده‌ایم:


و یا می‌توان بجای div، از المان anchor نیز استفاده کرد تا به صورت لینک نمایش داده شود:
<a class="navbar-brand" href="#"> Wisdom Pet Medicine </a>
و بجای متن، تصاویر را نیز می‌توان قرار داد.

برای تعریف متنی در navbar از کلاس navbar-text استفاده می‌شود:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <a class="navbar-brand d-none d-sm-inline-block" href="#"> Wisdom
                Pet Medicine </a>
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
            <span class="navbar-text d-none d-xl-inline-block">The best in
                traditional and alternate
                medicine</span>
        </div>
    </nav>

    <div class="container">
اما چون این متن طولانی است، بهتر است آن‌را در اندازه‌ی صفحه‌ی xl نمایش دهیم. به همین جهت با افزودن کلاس d-none، آن‌را در تمام اندازه‌ها مخفی می‌کنیم. سپس با افزودن d-xl-inline-block، آن‌را پس از عرض xl نمایان خواهیم کرد.
همین تنظیم را به navbar-brand، در اندازه‌ی sm نیز اضافه کرده‌ایم تا لوگوی سایت در اندازه‌های موبایل ظاهر نشود.



افزودن drop downs به navbar در بوت استرپ 4

برای تبدیل یکی از آیتم‌های منوی راهبری، به منو، از dropdown استفاده می‌شود که نمونه‌ای از آن‌را در مثال زیر مشاهده می‌کنید:
<body>
    <nav class="navbar bg-dark navbar-dark navbar-expand-sm">
        <div class="container">
            <a class="navbar-brand d-none d-sm-inline-block" href="#">
                <img src="images/wisdompetlogo.svg" style="width:40px;" alt="">
                Wisdom Pet Medicine
            </a>
            <div class="navbar-nav ml-sm-auto">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>

                <div class="dropdown">
                    <a class="nav-item nav-link dropdown-toggle" data-toggle="dropdown"
                        id="servicesDropdown" aria-haspopup="true"
                        aria-expanded="false" href="#">Services</a>
                    <div class="dropdown-menu" aria-labelledby="servicesDropdown">
                        <a href="#" class="dropdown-item">Grooming</a>
                        <a href="#" class="dropdown-item">General Health</a>
                        <a href="#" class="dropdown-item">Nutrition</a>
                        <a href="#" class="dropdown-item">Pest Control</a>
                        <a href="#" class="dropdown-item">Vaccinations</a>
                    </div>
                </div>

                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link disabled" href="#">Testimonials</a>
            </div>
            <span class="navbar-text d-none d-xl-inline-block">The best in
                traditional and alternate
                medicine</span>
        </div>
    </nav>

    <div class="container">
با این خروجی:


- دراپ‌داون نیاز به یک container دارد که آن‌را با تعریف یک div با کلاس dropdown تعریف کرده‌ایم.
- سپس به لینکی که قرار است آن‌را نمایش دهد، کلاس dropdown-toggle را اضافه می‌کنیم تا آیکن مثلثی رو به پایینی را نمایان کند. وجود این مثلث، بیانگر وجود منویی به همراه آن است.
- اکنون با تنظیم data-toggle به dropdown، کدهای جاوا اسکریپتی بوت استرپ، این المان را به صورت یک dropdown پردازش می‌کنند و نیازی به افزودن اسکریپتی به صفحه برای فعالسازی آن نیست. ویژگی‌های aria-expanded و aria-haspopup نیز به مقدار دهی پیش‌فرض‌های کدهای جاوا اسکریپتی آن کمک می‌کنند.
- خود منو توسط دربرگیرنده‌ای با کلاس dropdown-menu و آیتم‌هایی با کلاس dropdown-item تشکیل می‌شود.
- در ادامه برای متصل کردن این دربرگیرنده به لینک نمایش دهنده‌ی منو، یک id را به لینک انتساب می‌دهیم (به نام servicesDropdown) و سپس aria-labelledby دربرگیرنده را به این id، مقدار دهی می‌کنیم.
- در این مثال با استفاده از کلاس navbar-nav ml-sm-auto، سبب شده‌ایم تا منوی سایت، از لبه‌ی سمت راست صفحه پس از عرض sm، شروع شود.


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

برای اضافه کردن المان‌های فرم به منوی راهبری سایت، ابتدا نیاز است کلاس form-inline را بر روی container این فرم قرار داد و سپس به ورودی‌های این فرم، کلاس form-control را اضافه می‌کنیم. اگر نیاز بود، توسط کلاس‌های margin و padding مخصوص بوت استرپ 4 مانند mr-2 نیز می‌توان بین آن‌ها فاصله ایجاد کرد:
<body>
    <nav class="navbar navbar-dark bg-dark navbar-expand-sm">
        <div class="container">
            <div class="navbar-nav">
                <a class="nav-item nav-link active" href="#">Home</a>
                <a class="nav-item nav-link" href="#">Mission</a>
                <a class="nav-item nav-link" href="#">Services</a>
                <a class="nav-item nav-link" href="#">Staff</a>
                <a class="nav-item nav-link" href="#">Testimonials</a>
            </div>
            <form class="form-inline">
                <input type="text" placeholder="Search..." class="form-control mr-2">
                <button class="btn btn-outline-light" type="submit">Go</button>
            </form>
        </div>
    </nav>

    <div class="container">
با این خروجی:


بوت استرپ در اندازه‌ی بزرگتر صفحه، فرم را به سمت راست و آیتم‌های منو را در سمت چپ نمایش می‌دهد.


کنترل محل قرارگیری المان‌ها در منوی راهبری سایت

توسط کلاس‌هایی مانند fixed-top (قرار گرفتن در بالای صفحه)، fixed-bottom (قرار گرفتن در پایین صفحه) و sticky-top، می‌توان محل قرارگیری منوی راهبری را تغییر داد. این کلاس‌ها را در مطلب «طرحبندی صفحات وب با بوت استرپ 4 - قسمت دوم» پیشتر بررسی کردیم.
برای توضیح حالت sticky-top، فرض کنید بالای منو، تصویر بزرگی از لوگوی سایت را دارید و این منو زیر آن قرار گرفته‌است. زمانیکه صفحه به سمت پایین اسکرول می‌شود، این منو نیز پایین خواهد آمد تا جائیکه در لبه‌ی بالای صفحه قرار گیرد. پس از آن، این منو در همین ناحیه باقی مانده و شبیه به fixed-top عمل می‌کند.

یک نکته: اگر fixed-bottom را مورد استفاده قرار دادید:
<nav class="navbar navbar-dark bg-dark navbar-expand-sm fixed-bottom">
 ممکن است متن پایین صفحه زیر این منو قرار گیرد و قابل خوانده شدن نباشد. برای این منظور می‌توان از کلاس margin-bottom بر روی container استفاده کرد:
<div class="container mb-5">



اضافه کردن منوی همبرگری به منوی راهبری سایت

در مورد کلاس navbar-expand-sm در این مطلب توضیح دادیم. هرچند قابلیت عمودی و افقی شدن خودکار آیتم‌های منوی راهبری بسیار جالب و کاربری است، اما در صفحات نمایشی کوچک، این نمایش عمودی می‌تواند ارتفاع قابل ملاحظه‌ای را به خود اختصاص دهد. به همین جهت می‌خواهیم نمایش آیتم‌های آن‌را وابسته به تصمیم کاربر کنیم.
<body>
    <nav class="navbar navbar-dark bg-dark navbar-expand-sm">
        <div class="container">
            <a href="#" class="navbar-brand">Wisdom Pet Medicine</a>
            <button class="navbar-toggler" type="button" data-toggle="collapse"
                data-target="#myToggle" aria-controls="myToggle" aria-expanded="false"
                aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="myToggle">
                <div class="navbar-nav">
                    <a class="nav-item nav-link active" href="#">Home</a>
                    <a class="nav-item nav-link" href="#">Mission</a>
                    <a class="nav-item nav-link" href="#">Services</a>
                    <a class="nav-item nav-link" href="#">Staff</a>
                    <a class="nav-item nav-link" href="#">Testimonials</a>
                </div>
            </div>
        </div>
    </nav>

    <div class="container">
- در اینجا نحوه‌ی پیاده سازی منوی همبرگری را در بوت استرپ 4 ملاحظه می‌کنید.
- ابتدا نیاز است دکمه‌ی این منو اضافه شود که توسط کلاس navbar-toggler مشخص شده‌است. سپس با توجه به اینکه این کامپوننت توسط کدهای جاوا اسکریپتی بوت استرپ کار می‌کند، اطلاعات مورد نیاز آن‌را توسط ویژگی‌های data-toggle، data-target و aria مشخص می‌کنیم.
- این دکمه نیاز دارد تا به یک div با کلاس collapse navbar-collapse متصل شود. این اتصال نیز از طریق id آن صورت می‌گیرد که در ویژگی data-target مقدار دهی شده‌است.
- اگر این دکمه را پس از navbar-brand قرار دهیم، در سمت چپ صفحه و اگر پیش از آن قرار دهیم، در سمت راست صفحه ظاهر می‌شود.

در حالت نمایش sm، آیتم‌های منو مخفی شده:


با کلیک بر روی دکمه‌ی منوی همبرگری آن، گزینه‌های منو نمایش داده می‌شوند:


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




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: Bootstrap4_07.zip
مطالب
کار با اسکنر در برنامه های تحت وب (قسمت دوم و آخر)

در قسمت قبل « کار با اسکنر در برنامه‌های تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژه‌ی عملی و پیاده سازی مطالب مطرح شده می‌رویم.

ابتدا پروژه‌ی   WCF را شروع می‌کنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژه‌ی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :

    [ServiceContract]
    public interface IScannerService
    {
        [OperationContract]
        [WebInvoke(Method = "GET",
           BodyStyle = WebMessageBodyStyle.Wrapped,
           RequestFormat = WebMessageFormat.Json,
           ResponseFormat = WebMessageFormat.Json,
           UriTemplate = "GetScan")]
        string GetScan();
    }
در اینجا ما فقط اعلان متدهای مورد نیاز خود را ایجاد کرده‌ایم. علت استفاده از Attribute ایی با نام WebInvoke ، مشخص نمودن نوع خروجی به صورت Json است و همچنین عنوان آدرس مناسبی برای صدا زدن متد. پس از آن کلاس ScannerService را مطابق کدهای زیر تغییر دهید:
    public class ScannerService : IScannerService
    {
        public string GetScan()
        {
            // TODO Add code here
        }
    }
تا اینجا فقط یک WCF Service معمولی ساخته‌ایم .در ادامه به سراغ کلاس WIA برای ارتباط با اسکنر می‌رویم.
بر روی پروژه‌ی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینه‌ی Microsoft Windows Image Acquisition Library v2.0 را به پروژه‌ی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر می‌شود که ارجاعی از آن را در کلاس ScannerService قرار می‌دهیم.
using WIA;
اکنون متد GetScan را مطابق زیر اصلاح می‌نماییم:
        public string GetScan()
        {
            var imgResult = String.Empty;
            var dialog = new CommonDialogClass();
            try
            {
                // نمایش فرم پیشفرض اسکنر
                var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType);
                
                // ذخیره تصویر در یک فایل موقت
                var filename = Path.GetTempFileName();
                image.SaveFile(filename);
                var img = Image.FromFile(filename);

                // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به 
                imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg);
            }
            catch
            {
                // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی 
                // بازگردانده می‌شود که به معنای نبود تصویر می‌باشد
            }

            return imgResult;
        }
دقت داشته باشید که کدها را در زمان توسعه بین Try..Catch قرار ندهید چون ممکن‌است در این زمان به خطاهایی برخورد کنید که نیاز باشد در مرورگر آنها را دیده و رفع خطا نمایید.
CommonDialogClass  کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر می‌باشد و متد‌های مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار می‌دهد که بسته به نیاز خود می‌توانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage می‌باشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش می‌دهد و کاربر از طریق آن می‌تواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :


و سپس فرم پیش فرض اسکنر‌های TWAIN را جهت تعیین تنظیمات اسکن نمایش می‌دهد که این امکان نیز در این فرم فراهم است تا دستگاه‌های Feeder یا Flated انتخاب گردند.

خروجی این متد همان عکس اسکن شده است که از نوع WIA.ImageFile می‌باشد و ما پس از دریافتش، ابتدا آن را در یک فایل موقت ذخیره نموده و سپس با استفاده از یک متد کمکی آن را به فرمت Base64 برای درخواست کننده اسکن ارسال می‌نماییم.

کدهای کلاس کمکی ImageHelper:

        public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format)
        {
            if (image != null)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    // Convert Image to byte[]
                    image.Save(ms, format);
                    byte[] imageBytes = ms.ToArray();

                    // Convert byte[] to Base64 String
                    string base64String = Convert.ToBase64String(imageBytes);
                    return base64String;
                }
            }
            return String.Empty;
        }
توجه داشته باشید که خروجی این متد قرار است توسط callBack یک متد جاوا اسکریپتی مورد استفاده قرار گرفته و احیانا عکس مورد نظر در صفحه نمایش داده شود. پس بهتر است که از قالب تصویر به شکل Base64 استفاده گردد. ضمن اینکه پلاگین‌های Jquery مرتبط با ویرایش تصویر هم از این قالب پشتیبانی می‌کنند. (اینجا )

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

کار ما تا اینجا در پروژه‌ی WCF Service تقریبا تمام است. اگر پروژه را یکبار Build نمایید برای اولین بار احتمالا پیغام خطاهای زیر ظاهر خواهند شد:


جهت رفع این خطا، در قسمت Reference‌های پروژه خود، WIA را انتخاب نموده و از Properties‌های آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل می‌شود.

به سراغ پروژه‌ی ویندوز فرم جهت هاست کردن این WCF سرویس می‌رویم. می‌توانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده می‌کنیم.
یک پروژه‌ی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژه‌ی قبلی رفته و dll‌‌های آن را به پروژه خود اضافه نمایید.
Form1.cs  را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
        private readonly Uri _baseAddress = new Uri("http://localhost:6019");
        private ServiceHost _host;
برای استفاده از کلاس ServiceHost لازم است تا ارجاعی به فضای نام System.ServiceModel داده شود. متغیر baseAddress_ نگه دارنده‌ی آدرس ثابت سرویس اسکنر در سمت کلاینت می‌باشد و به این ترتیب ما دقیقا می‌دانیم باید سرویس را با کدام آدرس در کدهای جاوا اسکریپتی خود فراخوانی نماییم.
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه می‌نماییم:
        private void Form1_Load(object sender, EventArgs e)
        {
            _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress);
            _host.Open();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            _host.Close();
        }
همین چند خط برای هاست کردن سرویس روی آدرس localhost و پورت 8010 کامپیوتر کلاینت کافی است. اما یکسری تنظیمات مربوط به خود سرویس هم وجود دارد که باید در زمان پیاده سازی سرویس، در خود پروژه‌ی سرویس، ایجاد می‌گردید. اما از آنجا که ما قرار است سرویس را در یک پروژه‌ی دیگر هاست کنیم، بنابراین این تنظیمات را باید در همین پروژه‌ی WinForm قرار دهیم.
فایل App.Config پروژه‌ی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="BehaviourMetaData">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="WcfServiceScanner.ScannerService"
               behaviorConfiguration="BehaviourMetaData">
        <endpoint address=""
                  binding="basicHttpBinding"
                  contract="WcfServiceScanner.IScannerService" />
      </service>
    </services>
  </system.serviceModel>

</configuration>
اکنون پروژه‌ی هاست آماده اجرا می‌باشد. اگر آنرا اجرا کنید و در مرورگر خود آدرس ذکر شده را وارد کنید، صفحه‌ی زیر را مشاهده خواهد کرد که به معنی صحت اجرای سرویس اسکنر می‌باشد.

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


به سراغ پروژه‌ی بعدی، یعنی وب سایت خود می‌رویم. یک پروژه‌ی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه می‌کنیم.
( از آنجا که کدها به صورت جاوا اسکریپت می‌باشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحه‌ی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a>
<img src="" id="img-scanned" />
<script>
    $("#get-scan").click(function () {
        var url = 'http://localhost:6019/';
        $.get(url, function (data) {
            $("#img-scanned").attr("src","data:image/Jpeg;base64,  "+ data.GetScanResult);
        });
    });
</script>
دقت کنید در هنگام دریافت اطلاعات از سرویس، نتیجه به شکل GetScanResult خواهد بود. الان اگر پروژه را اجرا نمایید و روی لینک کلیک کنید، اسکنر شروع به دریافت اسکن خواهد کرد اما نتیجه‌ای بازگشت داده نخواهد شد و علت هم مشکل امنیتی CORS می‌باشد که به دلیل دریافت اطلاعات از یک دامین دیگر رخ می‌دهد و اگر با Firebug درخواست را بررسی کنید متوجه خطا به شکل زیر خواهید شد.


راه حل‌های زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژه‌ی ما جوابگو نمی‌باشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژه‌ی WCF Service  می‌باشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
    public class CORSSupport : IDispatchMessageInspector
    {
        Dictionary<string, string> requiredHeaders;
        public CORSSupport(Dictionary<string, string> headers)
        {
            requiredHeaders = headers ?? new Dictionary<string, string>();
        }

        public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
            var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty;
            if (httpRequest.Method.ToLower() == "options")
                instanceContext.Abort();
            return httpRequest;
        }

        public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
            var httpRequest = correlationState as HttpRequestMessageProperty;

            foreach (var item in requiredHeaders)
            {
                httpResponse.Headers.Add(item.Key, item.Value);
            }
            var origin = httpRequest.Headers["origin"];
            if (origin != null)
                httpResponse.Headers.Add("Access-Control-Allow-Origin", origin);

            var method = httpRequest.Method;
            if (method.ToLower() == "options")
                httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent;

        }
    }

    // Simply apply this attribute to a DataService-derived class to get
    // CORS support in that service
    [AttributeUsage(AttributeTargets.Class)]
    public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior
    {
        #region IServiceBehavior Members

        void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
        {
        }

        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var requiredHeaders = new Dictionary<string, string>();

            //Chrome doesn't accept wildcards when authorization flag is true
            //requiredHeaders.Add("Access-Control-Allow-Origin", "*");
            requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS");
            requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type");
            requiredHeaders.Add("Access-Control-Allow-Credentials", "true");
            foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
            {
                foreach (EndpointDispatcher ed in cd.Endpoints)
                {
                    ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders));
                }
            }
        }

        void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }

        #endregion
    }
فضاهای نام لازم برای این کلاس به شرح زیر می‌باشد:
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
کلاس ScannerService را باز کرده و آنرا به ویژگی
    [CORSSupportBehavior]
    public class ScannerService : IScannerService
    {
مزین نمایید.

کار تمام است، یکبار دیگر ابتدا پروژه‌ی WcfServiecScanner و سپس پروژه هاست را Build کرده و برنامه‌ی هاست را اجرا کنید. اکنون مشاهده می‌کنید که با زدن دکمه‌ی اسکن، اسکنر فرم تنظیمات اسکن را نمایش می‌دهد که پس از زدن دکمه‌ی Scan، پروسه آغاز شده و پس از اتمام، تصویر اسکن شده در صفحه‌ی وب سایت نمایش داده می‌شود.
مطالب
آشنایی با NHibernate - قسمت چهارم

در این قسمت یک مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمت‌های قبل با هم مرور خواهیم کرد. برای سادگی کار از یک برنامه Console استفاده خواهد شد (هر چند مرسوم شده است که برای نوشتن آزمایشات از آزمون‌های واحد بجای این نوع پروژه‌ها استفاده شود). همچنین فرض هم بر این است که database schema برنامه را مطابق قسمت قبل در اس کیوال سرور ایجاد کرده اید (نکته آخر بحث قسمت سوم).

یک پروژه جدید از نوع کنسول را به solution برنامه (همان NHSample1 که در قسمت‌های قبل ایجاد شد)، اضافه نمائید.
سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه کنید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمت‌های قبل تعاریف موجودیت‌ها و نگاشت‌ آن‌ها را در این پروژه class library ایجاد کرده بودیم و اکنون قصد استفاده از آن را داریم.

اگر دیتابیس قسمت قبل را هنوز ایجاد نکرده‌اید، کلاس CDb را به برنامه افزوده و سپس متد CreateDb آن‌را به برنامه اضافه نمائید.

using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);

PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);

var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
اکنون برای ایجاد دیتابیس اس کیوال سرور بر اساس نگاشت‌های قسمت قبل، تنها کافی است دستور ذیل را صادر کنیم:

CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());

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

در ادامه یک کلاس جدید به نام Config را به برنامه کنسول ایجاد شده اضافه کنید:

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اگر بحث را دنبال کرده باشید، این کلاس را پیشتر در کلاس FixtureBase آزمون واحد خود، به نحوی دیگر دیده بودیم. برای کار با NHibernate‌ نیاز به یک سشن مپ شده به موجودیت‌های برنامه می‌باشد که توسط متد CreateSessionFactory کلاس فوق ایجاد خواهد شد. این متد را به این جهت استاتیک تعریف کرده‌ایم که هیچ نوع وابستگی به کلاس جاری خود ندارد. در آن نوع دیتابیس مورد استفاده ( برای مثال اس کیوال سرور 2008 یا هر مورد دیگری که مایل بودید)، به همراه اسمبلی حاوی اطلاعات نگاشت‌های برنامه معرفی شده‌اند.

اکنون سورس کامل مثال برنامه را در نظر بگیرید:

کلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:

using System;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;

public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}

public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

Console.WriteLine("Saving a customer...");

session.Save(vahid);
session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();

return vahid.Id;
}
}
}

public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);

Console.WriteLine("Deleting a customer...");
session.Delete(customer);

session.Flush();//چندین عملیات با هم و بعد

transaction.Commit();
}
}
}
}
}
و سپس استفاده از آن در برنامه

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضیحات:
نیاز است تا ISessionFactory را برای ساخت سشن‌های دسترسی به دیتابیس ذکر شده در تنظمیات آن جهت استفاده در تمام تردهای برنامه، ایجاد نمائیم. لازم به ذکر است که تا قبل از فراخوانی BuildSessionFactory این تنظیمات باید معرفی شده باشند و پس از آن دیگر اثری نخواهند داشت.
ایجاد شیء ISessionFactory هزینه بر است و گاهی بر اساس تعداد کلاس‌هایی که باید مپ شوند، ممکن است تا چند ثانیه به طول انجامد. به همین جهت نیاز است تا یکبار ایجاد شده و بارها مورد استفاده قرار گیرد. در برنامه به کرات از using استفاده شده تا اشیاء IDisposable را به صورت خودکار و حتمی، معدوم نماید.

بررسی متد AddNewCustomer :
در ابتدا یک سشن را از ISessionFactory موجود درخواست می‌کنیم. سپس یکی از بهترین تمرین‌های کاری جهت کار با دیتابیس‌ها ایجاد یک تراکنش جدید است تا اگر در حین اجرای کوئری‌ها مشکلی در سیستم، سخت افزار و غیره پدید آمد، دیتابیسی ناهماهنگ حاصل نشود. زمانیکه از تراکنش استفاده شود، تا هنگامیکه دستور transaction.Commit آن با موفقیت به پایان نرسیده باشد، اطلاعاتی در دیتابیس تغییر نخواهد کرد و از این لحاظ استفاده از تراکنش‌ها جزو الزامات یک برنامه اصولی است.
در ادامه یک وهله از شیء Customer را ایجاد کرده و آن‌را مقدار دهی می‌کنیم (این شیء در قسمت‌های قبل ایجاد گردید). سپس با استفاده از session.Save دستور ثبت را صادر کرده، اما تا زمانیکه transaction.Commit فراخوانی و به پایان نرسیده باشد، اطلاعاتی در دیتابیس ثبت نخواهد شد.
نیازی به ذکر سطر فلاش در این مثال نبود و NHibernate اینکار را به صورت خودکار انجام می‌دهد و فقط از این جهت عنوان گردید که اگر چندین عملیات را با هم معرفی کردید، استفاده از session.Flush سبب خواهد شد که رفت و برگشت‌ها به دیتابیس حداقل شود و فقط یکبار صورت گیرد.
در پایان این متد، Id ثبت شده در دیتابیس بازگشت داده می‌شود.

چون در متد CreateSessionFactory ، متد ShowSql را نیز ذکر کرده بودیم، هنگام اجرای برنامه، عبارات SQL ایی که در پشت صحنه توسط NHibernate تولید می‌شوند را نیز می‌توان مشاهده نمود:



بررسی متد DeleteCustomer :
ایجاد سشن و آغاز تراکنش آن همانند متد AddNewCustomer است. سپس در این سشن، یک شیء از نوع Customer با Id ایی مشخص load‌ خواهد گردید. برای نمونه، نام این مشتری نیز در کنسول نمایش داده می‌شود. سپس این شیء مشخص و بارگذاری شده را به متد session.Delete ارسال کرده و پس از فراخوانی transaction.Commit ، این مشتری از دیتابیس حذف می‌شود.

برای نمونه خروجی SQL پشت صحنه این عملیات که توسط NHibernate مدیریت می‌شود، به صورت زیر است:

Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
NHibernate: INSERT INTO [Customer] (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
استفاده از دیتابیس SQLite بجای SQL Server در مثال فوق:

فرض کنید از هفته آینده قرار شده است که نسخه سبک و تک کاربره‌ای از برنامه ما تهیه شود. بدیهی است SQL server برای این منظور انتخاب مناسبی نیست (هزینه بالا برای یک مشتری، مشکلات نصب، مشکلات نگهداری و امثال آن برای یک کاربر نهایی و نه یک سازمان بزرگ که حتما ادمینی برای این مسایل در نظر گرفته می‌شود).
اکنون چه باید کرد؟ باید برنامه را از صفر بازنویسی کرد یا قسمت دسترسی به داده‌های آن‌را کلا مورد باز بینی قرار داد؟ اگر برنامه اسپاگتی ما اصلا لایه دسترسی به داده‌ها را نداشت چه؟! همه جای برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از یک دیتابیس دیگر یعنی باز نویسی کل برنامه.
همانطور که ملاحظه می‌کنید، زمانیکه با NHibernate کار شود، مدیریت لایه دسترسی به داده‌ها به این فریم ورک محول می‌شود و اکنون برای استفاده از دیتابیس SQLite تنها باید تغییرات زیر صورت گیرد:
ابتدا ارجاعی را به اسمبلی System.Data.SQLite.dll اضافه نمائید (تمام این اسمبلی‌های ذکر شده به همراه مجموعه FluentNHibernate ارائه می‌شوند). سپس:
الف) ایجاد یک دیتابیس خام بر اساس کلاس‌های domain و mapping تعریف شده در قسمت‌های قبل به صورت خودکار

CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
ب) تغییر آرگومان متد CreateSessionFactory

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...

نمایی از دیتابیس SQLite تشکیل شده پس از اجرای متد قسمت الف ، در برنامه Lita :




دریافت سورس برنامه تا این قسمت

نکته:
در سه قسمت قبل، تمام خواص پابلیک کلاس‌های پوشه domain را به صورت معمولی و متداول معرفی کردیم. اگر نیاز به lazy loading در برنامه وجود داشت، باید تمامی کلاس‌ها را ویرایش کرده و واژه کلیدی virtual را به کلیه خواص پابلیک آن‌ها اضافه کرد. علت هم این است که برای عملیات lazy loading ، فریم ورک NHibernate باید یک سری پروکسی را به صورت خودکار جهت کلاس‌های برنامه ایجاد نماید و برای این امر نیاز است تا بتواند این خواص را تحریف (override) کند. به همین جهت باید آن‌ها را به صورت virtual تعریف کرد. همچنین تمام سطرهای Not.LazyLoad نیز باید حذف شوند.

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


مطالب دوره‌ها
استفاده از Async و Await در برنامه‌های دسکتاپ
امکان استفاده از قابلیت‌های غیرهمزمان دات نت 4.5 در برنامه‌های WPF نیز به روش‌های مختلفی میسر است که در ادامه دو روش مرسوم آن‌را بررسی خواهیم کرد.

تهیه مقدمات بحث

ابتدا یک برنامه‌ی WPF جدید را آغاز کنید. سپس کدهای MainWindow.xaml آن‌را به نحو ذیل تغییر دهید.
<Window x:Class="Async10.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <DockPanel Dock="Top">
            <Button Name="BtnGo" Content="Go" Click="BtnGo_OnClick" />
            <ProgressBar Name="ProgressBar" IsIndeterminate="True" Visibility="Collapsed"/>
        </DockPanel>
        <TextBox Name="Results"/>
    </DockPanel>
</Window>
قصد داریم اطلاعاتی را از وب دریافت و سپس در TextBox قرار گرفته در صفحه نمایش دهیم.
در این مثال از کلاس جدید HttpClient نیز استفاده خواهیم کرد. برای استفاده از آن نیاز است ارجاعی را به اسمبلی استاندارد System.Net.Http.dll نیز به پروژه اضافه کنید.


روش اول

در ادامه کدهای فایل MainWindow.xaml.cs را به نحو ذیل تغییر داده و سپس برنامه را اجرا کنید.
using System.Net.Http;
using System.Windows;

namespace Async10
{
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void BtnGo_OnClick(object sender, RoutedEventArgs e)
        {
            BtnGo.IsEnabled = false;
            ProgressBar.Visibility = Visibility.Visible;

            var url = "https://www.dntips.ir";
            var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async");
            var task = client.GetStringAsync(url);
            task.ContinueWith(t =>
            {
                Results.Text = t.Result;

                BtnGo.IsEnabled = true;
                ProgressBar.Visibility = Visibility.Collapsed;
            });
        }
    }
}
روال رخدادگردان BtnGo_OnClick به نحو مرسوم آن نوشته شده است. بنابراین جهت دریافت نتیجه‌ی متد GetStringAsync می‌توان از متد ContinueWith بر روی task دریافت اطلاعات از وب، استفاده کرد. همچنین در اینجا مستقیما اطلاعات و نتیجه‌ی دریافتی را به عناصر UI انتساب داده‌ایم.
اگر پروژه را اجرا کنید، برنامه با استثنای زیر متوقف می‌شود:
 The calling thread cannot access this object because a different thread owns it.
چون task آغاز شده در ترد دیگری نسبت به ترد UI اجرا می‌شود، مجوز تغییری را در کدهای UI ندارد. برای حل این مشکل می‌توان از دو روش ذیل استفاده کرد:

الف) با استفاده از SynchronizationContext.Current و متد Post آن
 var context = SynchronizationContext.Current;
task.ContinueWith(t => context.Post(state =>
{
   Results.Text = t.Result;

   BtnGo.IsEnabled = true;
   ProgressBar.Visibility = Visibility.Collapsed;
}, null));
با این روش در قسمت اول آشنا شدید. SynchronizationContext.Current در اینجا چون در ابتدای متد و خارج از ContinueWith دریافت اطلاعات، اجرا می‌شود، به ترد UI یا ترد اصلی برنامه اشاره می‌کند. سپس همانطور که ملاحظه می‌کنید، توسط متد Post آن می‌توان اطلاعات را در زمینه‌ی تردی که SynchronizationContext به آن اشاره می‌کند اجرا کرد.

ب) با استفاده از امکانات TaskScheduler
 var taskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
task.ContinueWith(t =>
{
   Results.Text = t.Result;

   BtnGo.IsEnabled = true;
   ProgressBar.Visibility = Visibility.Collapsed;
}, taskScheduler);
وقتی یک task اجرا می‌شود، TPL یا task parallel library نیاز دارد بداند، این task بر روی چه تردی و چه زمانی قرار است اجرا شود. به صورت پیش فرض از thread pool استفاده می‌کند، اما الزامی به آن نیست. با استفاده از TaskScheduler می‌توان بر روی نحوه‌ی رفتار تردهای TPL تاثیر گذاشت و یا حتی آن‌ها را سفارشی سازی کرد. متد FromCurrentSynchronizationContext، یک TaskScheduler جدید را در اختیار ما قرار می‌دهد که کدهای آن بر اساس SynchronizationContext.Current کار می‌کند؛ در اینجا Context به UI اشاره می‌کند و در یک برنامه‌ی وب، به یک درخواست رسیده.
برای مثال اگر در برنامه‌های وب یک Task جدید را اجرا کنید شاید اینطور به نظر برسد که به HttpContext دسترسی ندارید. این نقیصه را می‌توان توسط کار با SynchronizationContext جاری برطرف کرد.
در مثال فوق، چون taskScheduler پیش از فراخوانی متد ContinueWith ایجاد شده‌است، به ترد UI اشاره می‌کند. در این حالت برای نمایش اطلاعات در همان ترد اصلی برنامه کافی است این taskScheduler را به عنوان پارامتر متد ContinueWith معرفی کنیم.


روش دوم

در دات نت 4.5 می‌توان روال رخدادگردان تعریف شده را به صورت async نیز معرفی کرد (یعنی مجاز هستیم امضای متد پیش فرض تولید شده را تغییر دهیم):
 private async void BtnGo_OnClick(object sender, RoutedEventArgs e)
سپس استفاده از await در کدهای برنامه میسر خواهد شد:
        private async void BtnGo_OnClick(object sender, RoutedEventArgs e)
        {
            BtnGo.IsEnabled = false;
            ProgressBar.Visibility = Visibility.Visible;

            var url = "https://www.dntips.ir";
            var client = new HttpClient(); // make sure you have an assembly reference to System.Net.Http.dll
            client.DefaultRequestHeaders.UserAgent.ParseAdd("Test Async");
            Results.Text = await client.GetStringAsync(url);
            BtnGo.IsEnabled = true;
            ProgressBar.Visibility = Visibility.Collapsed;
        }
در این حالت دیگر نیازی به استفاده از ContinueWith و مباحث SynchronizationContext نیست. زیرا تمام آن‌ها به صورت توکار اعمال می‌شوند. به علاوه کدنهایی نیز بسیار خواناتر شده‌است.
مطالب
از NET Standard. به NET 5.
 ارائه‌ی NET 5. یا پایان NET Standard.

تا پیش از ارائه‌ی NET 5.، پیاده سازی‌های مجزایی از دات نت مانند Full .NET Framework ،.NET Core ،Xamarin و غیره وجود داشتند و دارند. در این حالت برای اینکه بتوان یک class library قابل اجرای بر روی تمام این‌ها را ارائه داد، نیاز به ارائه‌ی API ای بود که بین تمام آن‌ها به اشتراک گذاشته شود و این دقیقا هدف وجودی NET Standard. است؛ اما ... مشکلات زیر را نیز به همراه دارد:
هر زمانیکه نیاز به افزودن یک API جدید باشد، نیاز خواهد بود تا یک نگارش جدید از NET Standard. ارائه شود. سپس تمام نگارش‌های مختلف دات نت باید سعی کنند به صورت مجزایی این API جدید را پیاده سازی کنند. این پروسه بسیار کند است. همچنین همواره باید مراجع مختلف را دقیق بررسی کنید که برای مثال کدام نگارش از دات نت، کدام نگارش از NET Standard. را پیاده سازی کرده‌است.
با ارائه‌ی NET 5.، وضعیت کاملا فرق کرده‌است. در اینجا یک «کد پایه‌ی اشتراکی» را برای تمام نگارش‌های مختلف دات نت داریم و این نگارش می‌خواهد مخصوص دسکتاپ یا برنامه‌های موبایل باشد، تفاوتی نمی‌کند. اکنون که تمام نگارش‌های مختلف دات نت بر فراز یک کد اشتراکی پایه کار می‌کنند، دیگر نیازی به ارائه‌ی مجزای یک API استاندارد و سپس پیاده سازی مجزای دیگری از آن، نیست.


نگارش‌های ویژه‌ی NET 5.، مخصوص سکوهای کاری مختلف

همانطور که عنوان شد، NET 5. در اصل یک «مجموعه کد اشتراکی» بین NET Core ،Mono ،Xamarin. و سایر پیاده سازی‌های دات نت است. اما سکوهای کاری مختلف، مانند Android، iOS، ویندوز و غیره، به همراه کدهای قابل توجهی که مختص به آن سیستم عامل‌های خاص باشند نیز هستند. برای رفع این مشکل، یکسری TFM یا target framework name/Target Framework Moniker ارائه شده‌اند. برای مثال net5.0 یکی از آن‌ها است. زمانیکه از این TFM استفاده می‌کنید، یعنی در حال کار با API ای هستید که در تمام نگارش‌های چندسکویی مختلف دات نت، مهیا و قابل استفاده‌است. نام جدید «net5.0» جایگزین کننده‌ی نام‌های قدیمی «netcoreapp» و «netstandard» است.
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
</Project>
در اینجا اگر نیاز به کار با API مخصوص ویندوز را نیز داشتید، می‌توانید از TFM مخصوص آن که net5.0-windows نام دارد استفاده کنید. «net5.0-windows» به این معنا است که به تمام امکانات net5.0 دسترسی دارید؛ به علاوه‌ی API ای که مختص به سیستم عامل ویندوز است (مانند Windows Forms و WPF). در دات نت 6، دو TFM جدید net6.0-android و net6.0-ios را نیز شاهد خواهیم بود.
کار کردن با این اسامی و درک آن‌ها نیز ساده‌است. برای مثال net6.0-ios به این معنا است که این قطعه کد و اسمبلی آن، می‌تواند تمام کتابخانه‌های مخصوص net5.0 و net6.0 را نیز استفاده کند؛ اما نه کتابخانه‌هایی که به صورت اختصاصی برای net5.0-windows و یا net6.0-android کامپایل شده‌اند.
این توضیحات به معنای پایان کار «NET Standard.» است. پس از NET Standard 2.1. فعلی، دیگر هیچ نگارش جدیدتری از آن ارائه نخواهد شد و البته net5.0 و تمام نگارش‌های پس از آن، قابلیت استفاده‌ی از کتابخانه‌های مبتنی بر NET Standard 2.1. و پیش از آن‌را نیز دارا هستند. بنابراین از این پس، net5.0 را به عنوان پایه‌ی به اشتراک گذاری کدها مدنظر داشته باشید.


سؤال: اگر امروز خواستیم کتابخانه‌ای را تولید کنیم، باید از کدام TFM استفاده کرد؟

net5.0 و تمام نگارش‌های پس از آن، از کتابخانه‌های مبتنی بر NET Standard 2.1. و قبل از آن نیز پیشیبانی می‌کنند. در این حالت تنها دلیل تغییر TFM کتابخانه‌های قدیمی موجود به net5.0، می‌تواند دسترسی به یکسری API جدید باشد. اما در مورد کتابخانه‌های جدید چطور؟ آیا باید برای مثال از netstandard2.0 استفاده کرد و یا از net5.0؟ این مورد بستگی به نوع پروژه‌ی در حال تهیه دارد:
- اجزای مختلف یک برنامه: اگر از کتابخانه‌ها برای شکستن برنامه‌ی خود به چندین قسمت استفاده می‌کنید، بهتر است از نام جدید netX.Y استفاده کنید (مانند net5.0). که در اینجا X.Y، منظور پایین‌ترین شماره نگارش NET. ای است که برنامه‌ی شما قرار است بر مبنای آن تهیه شود.
- کتابخانه‌های با قابلیت استفاده‌ی مجدد: اگر قرار است از کتابخانه‌ی شما در NET 4x. نیز استفاده شود، با netstandard2.0 شروع کنید. بهتر است netstandard1x را فراموش کنید؛ چون دیگر پشتیبانی نمی‌شود. اگر نیازی به پشتیبانی از NET 4x. ندارید، می‌توانید یا از netstandard2.1 و یا از net5.0 استفاده کنید و اگر در این حالت نیازی به پشتیبانی از NET Core 3x. ندارید، می‌توانید مستقیما با net5.0 شروع کنید.
بنابراین به صورت خلاصه:
- netstandard2.0 امکان اشتراک کدها را بین NET 4x. و سایر سکوهای کاری میسر می‌کند.
- netstandard2.1 امکان اشتراک کدها را بین Mono ،Xamarin و NET Core 3x. میسر می‌کند.
- net5.0، مخصوص نگارش فعلی و آینده‌ی دات نت است.

و یا حتی می‌توانید یک کتابخانه را به صورت multi-targeting با پشتیبانی از تمام TFMهای یاد شده‌ی فوق نیز تولید کنید:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net5.0;netstandard2.1;netstandard2.0;net461</TargetFrameworks>
  </PropertyGroup>
</Project>
مطالب
مستند سازی ASP.NET Core 2x API توسط OpenAPI Swagger - قسمت هفتم - سفارشی سازی ظاهر مستندات API
امکان کنترل کامل و سفارشی سازی ظاهر نهایی Swagger-UI وجود دارد که جزئیات آن‌را در این قسمت بررسی خواهیم کرد.


بهبود ظاهر کامنت‌ها با بکارگیری Markdown

Markdown زبان سبکی است برای تعیین شیوه‌نامه‌ی نمایش متون ساده. اگر پیشتر با سیستم ارسال نظرات Github و یا Stackoverflow کار کرده باشید، قطعا با آن آشنایی دارید. توضیحات کامل و جزئیات آن‌را می‌توانید در آدرس markdownguide.org مطالعه کنید. خوشبختانه امکان استفاده‌ی از Markdown در OpenAPI spec نیز وجود دارد که سبب بهبود ظاهر مستندات نهایی حاصل از آن خواهد شد.
در قسمت سوم، سعی کردیم مثالی را توسط remarks، به قسمت Patch اضافه کنیم که ظاهر آن، آنچنان مطلوب به نظر نمی‌رسد و بهتر است آن‌را به صورت یک قطعه کد نمایش داد:


برای بهبود این ظاهر می‌توان از Markdown استفاده کرد. بنابراین ابتدا تمام backslash‌های اضافه شده را که جهت نمایش خطوط جدید اضافه شده بودند، حذف می‌کنیم. در Markdown خطوط جدید با درج حداقل 2 فاصله (space) و یک سطر جدید مشخص می‌شوند. همچنین استفاده‌ی از ** سبب ضخیم شدن نمایش عبارت‌ها می‌شود. برای اینکه قطعه کد نوشته شده را در Markdown به صورت کدی با پس زمینه‌ای مشخص نمایش دهیم، پیش از شروع هر سطر آن نیاز است یک tab و یا 4 فاصله (space) درج شوند:
/// <remarks>
/// Sample request (this request updates the author's **first name**)
///
///     PATCH /authors/id
///     [
///         {
///           "op": "replace",
///           "path": "/firstname",
///           "value": "new first name"
///           }
///     ]
/// </remarks>
پس از این تغییرات خواهیم داشت:

که نسبت به حالت قبلی بسیار بهتر به نظر می‌رسد.
در نگارش فعلی، استفاده‌ی از Markdown برای توضیحات remarks، پارامترها و response codes پشتیبانی می‌شود؛ اما نه برای قسمت summary که برای نگارش بعدی درنظر گرفته شده‌است. همچنین از قابلیت‌های پیشترفته‌ی Markdown هم استفاده نکنید (در کل نیاز به مقداری سعی و خطا دارد تا مشخص شود چه قابلیت‌هایی را پشتیبانی می‌کند).


سفارشی سازی مقدماتی UI به کمک تنظیمات API آن

جائیکه تنظیمات میان‌افزار Swashbuckle.AspNetCore در کلاس Starup تعریف می‌شوند، می‌توان تغییراتی را نیز در UI آن اعمال کرد:
namespace OpenAPISwaggerDoc.Web
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
    // ...
            app.UseSwaggerUI(setupAction =>
            {
                setupAction.SwaggerEndpoint(
                    url: "/swagger/LibraryOpenAPISpecification/swagger.json",
                    name: "Library API");
                setupAction.RoutePrefix = "";

                setupAction.DefaultModelExpandDepth(2);
                setupAction.DefaultModelRendering(Swashbuckle.AspNetCore.SwaggerUI.ModelRendering.Model);
                setupAction.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
                setupAction.EnableDeepLinking();
                setupAction.DisplayOperationId();
            });
    // ...
        }
    }
}
با این خروجی که به علت تنظیم DocExpansion آن به None، اینبار لیست قابلیت‌ها را به صورت باز شده نمایش نمی‌دهد:


همچنین چون DefaultModelRendering به Model تنظیم شده‌است، اینبار بجای مثال، مشخصات مدل را به صورت پیش‌فرض نمایش می‌دهد:


کار DisplayOperationId نمایش Id هر Operation است؛ مانند get_api_authors. در اینجا Operation همان مداخل API ما هستند و به عنوان هر قسمت، Tag گفته می‌شود؛ مانند Authors و یا Books:


با فعالسازی EnableDeepLinking، آدرس‌هایی مانند tagName/# و یا tagName/OperationId/# قابلیت مرور و بازشدن خودکار را پیدا می‌کنند (tagName همان نام کنترلر است و OperationId همان اکشن متدی که عمومی شده‌است). برای مثال اگر آدرس https://localhost:5001/index.html#/Books/get_api_authors__authorId__books را در یک برگه‌ی جدید مرورگر باز کنیم، بلافاصله گروه Books، باز شده و سپس به اکشن متد یا مدخلی که Id آن ذکر شده‌است، هدایت می‌شویم:



اعمال تغییرات پیشرفته در UI

Swagger-UI در اصل از یک سری فایل html، css، js و فونت تشکیل شده‌است که آن‌ها را می‌توانید در آدرس src/Swashbuckle.AspNetCore.SwaggerUI مشاهده کنید. برای مثال فایل index.html آن‌را در اینجا می‌توانید مشاهده کنید. اصل آن در div ای با id مساوی swagger-ui رخ می‌دهد و در این قسمت است که رابط کاربری آن به صورت پویا تولید شده و رندر خواهد شد. بررسی فایل‌های js و css آن در این مخزن کد مشکل است؛ چون نگارش فشرده شده‌ی آن هستند. به همین جهت می‌توان به مخزن کد اصلی Swagger-UI که نگارش جایگذاری شده‌ی آن (embedded) توسط Swashbuckle.AspNetCore ارائه می‌شود، رجوع کرد. برای مثال در پوشه‌ی src/styles آن، اصل فایل‌های css آن برای سفارشی سازی وجود دارند.

پس از اعمال تغییرات خود، می‌توانید css و یا js سفارشی خود را به نحو زیر به تنظیمات app.UseSwaggerUI سیستم معرفی کنید:
setupAction.InjectStylesheet("/Assets/custom-ui.css");
setupAction.InjectJavaScript("/Assets/custom-js.js");
باید دقت داشت که این فایل‌ها باید در پوشه‌ی wwwroot قرار گرفته و قابل دسترسی باشند.
برای اعمال تغییرات اساسی و تزریق صفحه‌ی index.html جدیدی، می‌توان به صورت زیر عمل کرد:
setupAction.IndexStream = () => 
    GetType().Assembly.GetManifestResourceStream(
  "OpenAPISwaggerDoc.Web.EmbeddedAssets.index.html");
نکته‌ی مهم: اینبار این فایل باید به صورت embedded ارائه شود. به همین جهت در مثال فوق، عبارت OpenAPISwaggerDoc.Web به فضای نام اصلی اسمبلی جاری اشاره می‌کند. سپس EmbeddedAssets، نام پوشه‌ای است که فایل index.html سفارشی سازی شده، در آن قرار خواهد گرفت. اکنون برای اینکه این فایل را به صورت EmbeddedResource معرفی کنیم، نیاز است فایل csproj را به نحو زیر ویرایش کرد:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <None Remove="EmbeddedAssets\index.html" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="EmbeddedAssets\index.html" />
  </ItemGroup>
</Project>

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: OpenAPISwaggerDoc-07.zip