مطالب
Blazor 5x - قسمت 26 - برنامه‌ی Blazor WASM - ایجاد و تنظیمات اولیه
در قسمت قبل، پایه‌ی Web API و سرویس‌های سمت سرور برنامه‌ی کلاینت Blazor WASM این سری را آماده کردیم. این برنامه‌ی سمت کلاینت، قرار است توسط عموم کاربران آن جهت رزرو کردن اتاق‌های هتل فرضی مثال این سری، مورد استفاده قرار گیرد. پیش از این نیز یک برنامه‌ی Blazor Server را تهیه کردیم که کار آن صرفا محدود است به مسائل مدیریتی هتل؛ مانند تعریف اتاق‌ها و امکانات رفاهی آن.


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

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


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

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

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

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

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

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

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

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

.invalid {
  outline: 1px solid red;
}

.validation-message {
  color: red;
}

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

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

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

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

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

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

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

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


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

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

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



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


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

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


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

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

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

        public DateTime EndDate { get; set; }

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


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

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

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime

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

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


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

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

<h3>HotelRooms</h3>

@code {

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

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


@code{
    HomeVM HomeModel = new HomeVM();

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


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-26.zip
مطالب
اعتبارسنجی سرویس های WCF
حالتی را در نظر بگیرید که سرویس‌های یک برنامه در آدرسی مشخص هاست شده اند. اگر اعتبار سنجی برای این سرویس‌ها در نظر گرفته نشود به راحتی می‌توان با در اختیار داشتن آدرس مورد نظر تمام سرویس های  برنامه را فراخوانی کرد و اگر رمزگذاری اطلاعات بر روی سرویس‌ها فعال نشده باشد می‌توان تمام اطلاعات این سرویس‌ها را به راحتی به دست آورد. کمترین تلاش در این مرحله برای پیاده سازی امنیت این است که برای فراخوانی هر سرویس حداقل یک شناسه و رمز عبور چک شود و فقط در صورتی که فراخوانی سرویس همراه با شناسه و رمز عبور درست بود اطلاعات در اختیار کلاینت قرار گیرد. قصد داریم طی یک مثال این مورد را بررسی کنیم:
ابتدا یک پروژه با دو Console Application  با نام های  Service و Client ایجاد کنید. سپس در پروژه Service یک سرویس به نام BookService ایجاد کنید و کد‌های زیر را در آن کپی نمایید:
Contract مربوطه به صورت زیر است:
[ServiceContract]
public interface IBookService
    {
        [OperationContract]
        int GetCountOfBook();
    }
کد‌های مربوط به سرویس:
 [ServiceBehavior(IncludeExceptionDetailInFaults = true)]
    public class BookService : IBookService
    {
        public int GetCountOfBook()
        {
            return 10;
        }
    }
فایل Program در پروژه Service را باز نمایید و کد‌های زیر را که مربوط به hosting سرویس مورد نظر است در آن کپی کنید:
 class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(BookService));

            var binding = new BasicHttpBinding();
            
            host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService");
            host.Open();

            Console.Write("BookService host");

            Console.ReadKey();
        }
    }
بر اساس کد‌های بالا، سرویس BookService در آدرس http://localhost/BookService هاست می‌شود.  نوع Binding نیز BasicHttpBinding انتخاب شده است.
حال نوبت به پیاده سازی سمت کلاینت می‌رسد. فایل Program سمت کلاینت را باز کرده و کد‌های زیر را نیز در آن کپی نمایید:
        static void Main(string[] args)
        {
            Thread.Sleep(2000);
            BasicHttpBinding binding = new BasicHttpBinding();

            ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService"));

            Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook());

            Console.ReadKey();
        }
در کد‌های عملیات ساخت ChannelFactory برای برقراری اطلاعات با سرویس مورد نظر انجام شده است. پروژه را Build نمایید و سپس آن را اجرا کنید.
 خروجی زیر مشاهده می‌شود:

تا اینجا هیچ گونه اعتبار سنجی انجام نشد. برای پیاده سازی اعتبار سنجی باید یک سری تنظیمات بر روی Binding و Hosting  سمت سرور و البته کلاینت بر قرار شود. فایل Program پروزه Service را باز نمایید و محتویات آن را به صورت زیر تغییر دهید:

 static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(BookService));

            var binding = new BasicHttpBinding();
            binding.Security = new BasicHttpSecurity();
            binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;

            host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom;

            host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator();               

            host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService");
            host.Open();


            Console.Write("BookService host");

            Console.ReadKey();
        }
تغییرات اعمال شده:
ابتدا نوع Security در Binding را به حالت TransportCredentialOnly تنظیم کردیم. در یک جمله هیچ گونه تضمینی برای صحت اطلاعات انتقالی در این حالت وجود ندارد و فقط یک اعتبار سنجی اولیه انجام خواهد شد. در نتیجه هنگام استفاده از این حالت باید با دقت عمل نمود و نباید فقط به پیاده سازی این حالت اکتفا کرد.( Encryption اطلاعات سرویس‌ها مورد بحث این پست نیست)
ClientCredentialType نیز باید به حالت Basic تنظیم شود. در WCF اعتبار سنجی به صورت پیش فرض در حالت Windows است (بعنی UserNamePasswordValidationMode برابر مقدار Windows است و اعتبار سنجی بر اساس کاربر انجام می‌شود) . این مورد باید به مقدار Custom تغییر یابد. در انتها نیز باید مدل اعتبار سنجی دلخواه خود را به صورت زیر پیاده سازی کنیم:
در پروژه سرویس یک کلاس به نام CustomUserNamePasswordValidator بسازید و کد‌های زیر را در آن کپی کنید:
 public class CustomUserNamePasswordValidator : UserNamePasswordValidator
    {
        public override void Validate(string userName, string password)
        {
            if (userName != "Masoud" || password != "Pakdel")
                throw new SecurityException("Incorrect userName or password");
        }
    }
Validator مورد نظر از کلاسی abstract به نام UserNamePasswordValidator  ارث می‌برد، در نتیجه باید متد abstract به نام Validate را override نماید. در بدنه این متد شناسه و رمز عبور با یک مقدار پیش فرض چک می‌شوند و در صورت عدم درستی این پارارمترها یک استثنا پرتاب خواهد شد.

تغییرات مورد نیاز سمت کلاینت:
اگر در این حالت پروژه را اجرا نمایید از آن جا که از این به بعد، درخواست‌ها سمت سرور اعتبار سنجی می‌شوند در نتیجه با خطای زیر روبرو خواهید شد:

این خطا از آن جا ناشی می‌شود که تنظیمات کلاینت و سرور از نظر امنیتی با هم تناسب ندارد. در نتیجه باید تنظیمات Binding کلاینت و سرور یکی شود. برای این کار کد زیر را به فایل Program سمت کلاینت اضافه می‌کنیم:


static void Main(string[] args)
        {
            Thread.Sleep(2000);
            BasicHttpBinding binding = new BasicHttpBinding();

            binding.Security = new BasicHttpSecurity();
            binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly;
            binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic;            

            ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService"));

            channel.Credentials.UserName.UserName = "WrongUserName";
            channel.Credentials.UserName.Password = "WrongPassword";

 Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook()); Console.ReadKey(); }
توسط دستور زیر، مقدار شناسه و رمز عبور به درخواست اضافه می‌شود.
channel.Credentials.UserName.UserName = "WrongUserName";
channel.Credentials.UserName.Password = "WrongPassword";
 در اینجا UserName و Password اشتباه مقدار دهی شده اند تا روش کار Validator مورد بررسی قرار گیرد. حال اگر پروژه را اجرا نمایید خواهید دید که در Validator مورد نظر، عملیات اعتبار سنجی به درستی انجام می‌شود:


دریافت سورس مثال بالا
مطالب
قرار دادن نمودارهای MS Chart در گزارشات PdfReport
در حالت کلی، هر شیءایی را که بتوان تبدیل به تصویر کرد، قابلیت قرارگیری در یک فایل PDF را هم خواهد داشت. از این نمونه می‌توان به اشیاء MSChart اشاره کرد که از دات نت 4 جزئی از کتابخانه‌های اصلی دات نت شده‌اند و البته برای دات نت سه و نیم نیز به صورت جداگانه قابل دریافت است.
در ادامه مثالی را بررسی خواهیم کرد که بر اساس ردیف‌های گزارش آن، یک نمودار، به انتهای گزارش اضافه خواهد شد.
کدهای کامل این مثال را در ذیل مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using PdfReportSamples.Models;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ChartImage
{
    public class ChartImagePdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            var chart = new MSChartHelper
                {
                    AxisXTitle = "User",
                    AxisYTitle = "Balance",
                    ChartTitle = "Users Balance",
                    AxisTitleFont = new System.Drawing.Font("Tahoma", 12f),
                    LabelStyleFont = new System.Drawing.Font("Tahoma", 10f),
                    ChartTitleFont = new System.Drawing.Font("Arial", 16f, System.Drawing.FontStyle.Bold),
                    LegendsFont = new System.Drawing.Font("Tahoma", 12f)
                };

            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
             .DefaultFonts(fonts =>
             {
                 fonts.Path(string.Format("{0}\\fonts\\irsans.ttf", AppPath.ApplicationPath),
                            string.Format("{0}\\fonts\\verdana.ttf", Environment.GetEnvironmentVariable("SystemRoot")));
             })
             .PagesFooter(footer =>
             {
                 footer.DefaultFooter(printDate: DateTime.Now.ToString("MM/dd/yyyy"));
             })
             .PagesHeader(header =>
             {
                 header.DefaultHeader(defaultHeader =>
                 {
                     defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                     defaultHeader.Message("گزارش جدید ما");
                 });
             })
             .MainTableTemplate(template =>
             {
                 template.BasicTemplate(BasicTemplate.ClassicTemplate);
             })
             .MainTablePreferences(table =>
             {
                 table.ColumnsWidthsType(TableColumnWidthType.Relative);
             })
             .MainTableDataSource(dataSource =>
             {
                 var listOfRows = new List<User>();
                 for (var i = 0; i < 7; i++)
                 {
                     listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = (i * 50) + 1000 });
                 }
                 dataSource.StronglyTypedList(listOfRows);
             })
             .MainTableEvents(events =>
             {
                 events.DataSourceIsEmpty(message: "There is no data available to display.");
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);
                 });
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
             })
             .MainTableSummarySettings(summary =>
             {
                 summary.OverallSummarySettings("جمع");
                 summary.PreviousPageSummarySettings("نقل از صفحه قبل");
             })
             .MainTableColumns(columns =>
             {
                 columns.AddColumn(column =>
                 {
                     column.PropertyName("rowNo");
                     column.IsRowNumber(true);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(0);
                     column.Width(1);
                     column.HeaderCell("ردیف", captionRotation: 90);
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Id);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(1);
                     column.Width(2);
                     column.HeaderCell("شماره");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Name);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(2);
                     column.Width(2);
                     column.HeaderCell("نام");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.LastName);
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(3);
                     column.Width(3);
                     column.HeaderCell("نام خانوادگی");
                 });

                 columns.AddColumn(column =>
                 {
                     column.PropertyName<User>(x => x.Balance);
                     column.HeaderCell("موجودی");
                     column.ColumnItemsTemplate(template =>
                     {
                         template.TextBlock();
                         template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.Width(2);
                     column.AggregateFunction(aggregateFunction =>
                     {
                         aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                         aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                     });
                     column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                     column.IsVisible(true);
                     column.Order(4);
                 });
             })
             .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\RptChartSample.pdf"));
        }
    }
}
برای تهیه این گزارش و افزودن نمودار به آن، از کلاس کمکی ذیل استفاده شده است:
using System.Drawing;
using System.IO;
//It's part of the .NET 4.0+ now.
using System.Windows.Forms.DataVisualization.Charting;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfRpt.Core.Helper;

namespace PdfReportSamples.ChartImage
{
    public class MSChartHelper
    {
        // MS Chart learning tutorials:
        // http://weblogs.asp.net/scottgu/archive/2010/02/07/built-in-charting-controls-vs-2010-and-net-4-series.aspx
        Chart _chart;

        public System.Drawing.Font AxisTitleFont { set; get; }

        public string AxisXTitle { set; get; }

        public string AxisYTitle { set; get; }

        public string ChartTitle { set; get; }

        public System.Drawing.Font ChartTitleFont { set; get; }

        public System.Drawing.Font LabelStyleFont { set; get; }

        public System.Drawing.Font LegendsFont { set; get; }

        public void AddChartToPage(Document pdfDoc,
                                   int scalePercent = 100,
                                   float spacingBefore = 20,
                                   float spacingAfter = 10,
                                   float widthPercentage = 80)
        {
            using (var chartimage = new MemoryStream())
            {
                _chart.SaveImage(chartimage, ChartImageFormat.Bmp); //BMP gives the best compression result

                var iTextSharpImage = PdfImageHelper.GetITextSharpImageFromByteArray(chartimage.GetBuffer());
                iTextSharpImage.ScalePercent(scalePercent);
                iTextSharpImage.Alignment = Element.ALIGN_CENTER;

                var table = new PdfPTable(1)
                {
                    WidthPercentage = widthPercentage,
                    SpacingBefore = spacingBefore,
                    SpacingAfter = spacingAfter
                };
                table.AddCell(iTextSharpImage);

                pdfDoc.Add(table);
            }
        }

        public void AddXY(object xValue, params object[] yValue)
        {
            _chart.Series[0].Points.AddXY(xValue, yValue);
        }

        public void ChartInit(int width, int height)
        {
            _chart = new Chart
            {
                Width = width,
                Height = height,
                AntiAliasing = AntiAliasingStyles.All,
                TextAntiAliasingQuality = TextAntiAliasingQuality.High,
                Palette = ChartColorPalette.BrightPastel,
                BackColor = ColorTranslator.FromHtml("#F3DFC1"),
                BackGradientStyle = GradientStyle.TopBottom
            };

            setBorder();
            setTitles();
            setChartAreas();
            setLegends();
            setSeries();
        }

        public void FreeResources()
        {
            if (_chart != null && !_chart.IsDisposed)
                _chart.Dispose();
        }        

        private void setBorder()
        {
            _chart.BorderSkin.SkinStyle = BorderSkinStyle.Emboss;
            _chart.BorderlineWidth = 2;
            _chart.BorderlineColor = Color.FromArgb(181, 64, 1);
            _chart.BorderlineDashStyle = ChartDashStyle.Solid;
        }

        private void setChartAreas()
        {
            _chart.ChartAreas.Add("ChartArea1");
            _chart.ChartAreas[0].AxisX.Title = AxisXTitle;
            _chart.ChartAreas[0].AxisY.Title = AxisYTitle;
            _chart.ChartAreas[0].AxisX.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisY.TitleFont = AxisTitleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Font = LabelStyleFont;
            _chart.ChartAreas[0].AxisX.LabelStyle.Angle = -90;
            _chart.ChartAreas[0].BackColor = Color.White;
            _chart.ChartAreas[0].AxisX.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisX.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.LineColor = Color.FromArgb(64, 64, 64);
            _chart.ChartAreas[0].AxisY.MajorGrid.LineColor = Color.FromArgb(64, 64, 64);
        }

        private void setLegends()
        {
            _chart.Legends.Add("Default");
            _chart.Legends[0].LegendStyle = LegendStyle.Row;
            _chart.Legends[0].IsTextAutoFit = false;
            _chart.Legends[0].DockedToChartArea = "ChartArea1";
            _chart.Legends[0].Docking = Docking.Bottom;
            _chart.Legends[0].IsDockedInsideChartArea = false;
            _chart.Legends[0].BackColor = Color.Transparent;
            _chart.Legends[0].Font = LegendsFont;
        }

        private void setSeries()
        {
            _chart.Series.Add("");
            _chart.Series[0].ChartType = SeriesChartType.Column;
            _chart.Series[0].Palette = ChartColorPalette.EarthTones;
            _chart.Series[0].IsValueShownAsLabel = true;
            _chart.Series[0].IsVisibleInLegend = false;
        }

        private void setTitles()
        {
            _chart.Titles.Add(ChartTitle);
            _chart.Titles[0].Font = ChartTitleFont;
            _chart.Titles[0].TextStyle = TextStyle.Shadow;
            _chart.Titles[0].ShadowOffset = 3;
            _chart.Titles[0].ShadowColor = Color.FromArgb(32, 0, 0);
            _chart.Titles[0].Alignment = ContentAlignment.TopCenter;
            _chart.Titles[0].ForeColor = Color.FromArgb(26, 59, 105);
        }
    }
}

توضیحات:
- استفاده از MSChart در اینجا از این جهت مناسب است که فراگیری کار کردن با آن عمومی بوده و در پروژه‌های وب و ویندوز کاربرد دارد و احتمالا هم اکنون با نحوه کارکردن با آن آشنا هستید، زیرا از سال 2010 به دات نت اضافه شده است.
- در این بین تنها متد جدید و مهم کلاس MSChartHelper، متد AddChartToPage آن است. به کمک متد chart.SaveImage می‌توان تصویر نهایی معادل یک نمودار را در حافظه ذخیره کرد. سپس با استفاده از متد PdfImageHelper.GetITextSharpImageFromByteArray، این تصویر موجود در حافظه را به معادل قابل استفاده آن در iTextSharp تبدیل کرده و به صفحه اضافه خواهیم کرد.
- در کدهای اصلی تولید گزارش، مقدار دهی کلاس کمکی MSChartHelper در قسمت رخدادهای قابل استفاده PdfReport در متد MainTableEvents آن انجام شده است:
در روال رویدادگردان DocumentOpened، بر اساس عرض واقعی صفحه، عرض نمودار را مشخص می‌کنیم:
                 events.DocumentOpened(args =>
                 {
                     chart.ChartInit(width: (int)args.PdfWriter.PageSize.Width - 100, height: 250);

                 });
سپس در روال رویدادگردان RowAdded، فرصت خواهیم داشت به اطلاعات در حال افزوده شدن به گزارش دسترسی داشته باشیم. این اطلاعات را به متد افزودن XY نمودار ارسال خواهیم کرد:
                 events.RowAdded(args =>
                 {
                     if (args.RowType == RowType.DataTableRow)
                     {
                         var name = args.TableRowData.GetValueOf<User>(x => x.Name);
                         if (name == null) return;

                         var balance = args.TableRowData.GetValueOf<User>(x => x.Balance);
                         if (balance == null) return;

                         chart.AddXY(name, balance);
                     }
                 });
و در آخر، پیش از بسته شدن فایل PDF تولیدی (DocumentClosing)، نمودار نهایی را به صفحه اضافه کرده و منابع مرتبط با آن‌را آزاد خواهیم کرد:
                 events.DocumentClosing(args =>
                 {
                     chart.AddChartToPage(args.PdfDoc);
                     chart.FreeResources();
                 });
بنابراین اگر این سؤال عمومی وجود دارد که آیا می‌توان در این بین، به ابتدا و انتهای گزارش اشیایی را افزود، روش کلی آن‌را در روال‌های فوق ملاحظه می‌کنید. توسط args.PdfDoc و args.PdfWriter می‌توان به زیرساخت iTextSharp دسترسی یافت.
 

نظرات مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
مثال فوق را (KendoUI06) اگر برای ASP.NET MVC بازنویسی کنیم به کدهای ذیل خواهیم رسید:
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Mvc;
using Kendo.DynamicLinq;
using KendoUI06Mvc.Models;
using Newtonsoft.Json;

namespace KendoUI06Mvc.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }

        [HttpDelete]
        public ActionResult DeleteProduct(int id)
        {
            var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return new HttpNotFoundResult();

            ProductDataSource.LatestProducts.Remove(item);

            return Json(item);
        }

        [HttpGet]
        public ActionResult GetProducts()
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
               this.Request.Url.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return Json(list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter),
                       JsonRequestBehavior.AllowGet);
        }

        [HttpPost]
        public ActionResult PostProduct(Product product)
        {
            if (!ModelState.IsValid)
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

            var id = 1;
            var lastItem = ProductDataSource.LatestProducts.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            product.Id = id;
            ProductDataSource.LatestProducts.Add(product);

            // گرید آی دی جدید را به این صورت دریافت می‌کند
            return Json(new DataSourceResult { Data = new[] { product } });
        }

        [HttpPut] // Add it to fix this error: The requested resource does not support http method 'PUT'
        public ActionResult UpdateProduct(int id, Product product)
        {
            var item = ProductDataSource.LatestProducts
                                        .Select(
                                            (prod, index) =>
                                                new
                                                {
                                                    Item = prod,
                                                    Index = index
                                                })
                                        .FirstOrDefault(x => x.Item.Id == id);
            if (item == null)
                return new HttpNotFoundResult();


            if (!ModelState.IsValid || id != product.Id)
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

            ProductDataSource.LatestProducts[item.Index] = product;

            //Return HttpStatusCode.OK
            return new HttpStatusCodeResult(HttpStatusCode.OK);
        }
    }
}
در این حالت View برنامه فقط جهت ذکر آدرس‌های جدید باید اصلاح شود و نیاز به تغییر دیگری ندارد:
        var productsDataSource = new kendo.data.DataSource({
            transport: {
                read: {
                    url: "@Url.Action("GetProducts","Home")",
                    dataType: "json",
                    contentType: 'application/json; charset=utf-8',
                    type: 'GET'
                },
                create: {
                    url: "@Url.Action("PostProduct","Home")",
                    contentType: 'application/json; charset=utf-8',
                    type: "POST"
                },
                update: {
                    url: function (product) {
                        return "@Url.Action("UpdateProduct","Home")/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (product) {
                            return "@Url.Action("DeleteProduct","Home")/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
در ASP.NET Core، برخلاف نگارش‌های قبلی ASP.NET که ASP.NET Web API مجزای از ASP.NET MVC و همچنین وب فرم‌ها ارائه شده بود، اکنون جزئی از ASP.NET MVC است و با آن یکپارچه می‌باشد. بنابراین پیشنیازهای راه اندازی Web API با ASP.NET Core شامل سه مورد ذیل هستند که پیشتر آن‌ها را بررسی کردیم:
الف) فعال سازی ارائه‌ی فایل‌های استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی

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


تعریف مسیریابی کلی کنترلر

در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، می‌توان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواست‌های آن‌را محدود به درخواست‌هایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
[controller] هم در اینجا یک توکن پیش فرض است که با نام کنترلر جاری یا همان Test، به صورت خودکار جایگزین می‌شود.
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن می‌توان برای ثبت استثناءهای رخ داده استفاده کرد.


یک کنترلر ، اما با قابلیت‌های متعدد

همانطور که ملاحظه می‌کنید، اینبار کلاس پایه‌ی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شده‌است و نه Api Controller سابق. تمام قابلیت‌های موجود در این‌دو توسط همان Controller ارائه می‌شوند.


هنوز پیش فرض‌های سابق Web API برقرار هستند

در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواست‌های Get ختم شده‌ی به نام کنترلر پاسخ می‌دهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا می‌شود.
- در اینجا می‌توان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف  <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، می‌توانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده می‌شود.
- تمام مسیریابی‌ها را توسط ویژگی Route و یا نوع‌های درخواستی مانند HttpGet، می‌توان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگی‌های مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller
{
    private readonly ILogger<TestController> _logger;
 
    public TestController(ILogger<TestController> logger)
    {
        _logger = logger;
    }
 
    [HttpGet]
    public IEnumerable<string> Get() // http://localhost:7742/api/test
    {
        return new [] { "value1", "value2" };
    }
 
    [HttpGet("{page:int}")]
    public IActionResult Get(int page) // http://localhost:7742/api/test/1
    {
        return Json(new[] { "value3", "value4" });
    }
 
    [HttpGet("/api/path1")]
    public IActionResult GetEpisodes1() // http://localhost:7742/api/path1
    {
        return Json(new[] { "value5", "value6" });
    }
 
    [HttpGet("/api/path2")]
    public IActionResult GetEpisodes2() // http://localhost:7742/api/path2
    {
        try
        {
            // get data from the DB ...
            return Ok(new[] { "value7", "value8" });
        }
        catch (Exception ex)
        {
            _logger.LogError("Failed to get data from the API", ex);
            return BadRequest();
        }
    } 
}
بنابراین در اینجا اگر می‌خواهید یک کنترلر ASP.NET Web API 2.x را به ASP.NET Core 1.0 ارتقاء دهید، تمام متدهای Get و Put و امثال آن هنوز معتبر هستند و مانند سابق عمل می‌کنند:
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET: api/values
        [HttpGet]
        public IEnumerable<string> Get()
        {
            return new string[] { "value1", "value2" };
        }
// GET api/values/5
        [HttpGet("{id}")]
        public string Get(int id)
        {
            return "value";
        }
// POST api/values
        [HttpPost]
        public void Post([FromBody]string value)
        {
        }
// PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody]string value)
        {
        }
// DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}
در مورد ویژگی FromBody در ادامه بیشتر بحث خواهد شد.

یک نکته: اگر می‌خواهید خروجی Web API شما همواره JSON باشد، می‌توانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
 [Produces("application/json")]
[Route("api/[controller]")] // http://localhost:7742/api/test
public class TestController : Controller


تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API

فرض کنید مدل زیر را به برنامه اضافه کرده‌اید:
namespace Core1RtmEmptyTest.Models
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}
و همچنین قصد دارید اطلاعات آن‌را از کاربر توسط یک عملیات POST دریافت کرده و به شکل JSON نمایش دهید:
using Core1RtmEmptyTest.Models;
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
    public class PersonController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
 
        [HttpPost]
        public IActionResult Index(Person person)
        {
            return Json(person);
        }
    }
}
برای اینکار، از jQuery به نحو ذیل استفاده می‌کنیم (از این جهت که بیشتر ارسال‌های به سرور جهت کار با Web API نیز Ajax ایی هستند):
@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.ajax({
                type: 'POST',
                url: '/Person/Index',
                dataType: 'json',
                contentType: 'application/json; charset=utf-8',
                data: JSON.stringify({
                    FirstName: 'F1',
                    LastName: 'L1',
                    Age: 23
                }),
                success: function (result) {
                    console.log('Data received: ');
                    console.log(result);
                }
            });
        });
    </script>
}


همانطور که مشاهده می‌کنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکرده‌است و مقادیر دریافتی نال هستند.
این مورد یکی از مهم‌ترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارش‌های قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمی‌شوند و باید به نحو ذیل، محل دریافت و تفسیر آن‌ها را دقیقا مشخص کرد:
 public IActionResult Index([FromBody]Person person)
زمانیکه ویژگی FromBody را مشخص می‌کنیم، آنگاه اطلاعات دریافتی از request body دریافتی، به شیء Person نگاشت خواهند شد.


نکته‌ی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.


حالت‌های دیگر تغییرات Model Binding در ASP.NET Core

تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامه‌ی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیده‌ی سمت سرور bind نمی‌شود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرم‌ها به سرور، همواره شامل دو تغییر ذیل است:
  var dataType = 'application/x-www-form-urlencoded; charset=utf-8';
 var data = $('form').serialize();
اطلاعات فرم سریالایز می‌شوند و data type مخصوصی هم برای آن‌ها تنظیم خواهد شد. در این حالت، ارسال یک چنین اطلاعاتی به سمت اکشن متد فوق، با خطای 415 unsupported media type متوقف می‌شود. برای رفع این مشکل باید از ویژگی دیگری به نام FromForm استفاده کرد:
 [HttpPost]
public IActionResult Index([FromForm]Person person)
حالت‌های دیگر ممکن را در تصویر ذیل ملاحظه می‌کنید:


علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارش‌های قبلی، تمام مکان‌ها و حالت‌های میسر جستجو می‌شوند و اگر یکی از آن‌ها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان می‌رسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا می‌کند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین می‌شوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالت‌ها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آن‌ها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل می‌کند:
در این حالت می‌توان در سازنده‌ی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel
{
    public ProductModel(IProductService prodService)
    {
        Value = prodService.Get(productId);
    }
    public IProduct Value { get; private set; }
}
این تزریق وابستگی‌ها برای اینکه تکمیل شود، نیاز به ویژگی FromServices خواهد داشت:
 public async Task<IActionResult> GetProduct([FromServices]ProductModel product)
{
}
وجود ویژگی FromServices به این معنا است که سرویس‌های مدل یاد شده را از تنظیمات ابتدایی IoC Container خود خوانده و سپس در اختیار مدل جاری قرار بده. به این ترتیب حتی تزریق وابستگی‌ها در مدل‌های برنامه هم میسر می‌شود.


تغییر تنظیمات اولیه‌ی خروجی‌های ASP.NET Web API

در اینجا حالت ارائه‌ی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوه‌ی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده می‌کنید:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
    {
        options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); 
 
    }).AddJsonOptions(options =>
    {
        options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include;
        options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
    });
همچنین اگر خواستید تنظیمات ابتدایی JSON.NET را تغییر داده و برای مثال خروجی JSON تولیدی را camel case کنید، این‌کار را توسط متد AddJsonOptions به نحو فوق می‌توان انجام داد.
مطالب
محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC
یک ادیتور آنلاین را تصور کنید که کاربران در قسمت ارسال تصویر آن قرار است فقط فایل‌های png، jpg و gif ارسال کنند و نه مثلا فایل test.aspx و موارد مشابه. در اینجا برای محدود کردن نوع فایل‌های آپلود شده می‌توان از فیلترهای سفارشی ASP.NET MVC کمک گرفت:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web.Mvc;

namespace SecurityModule
{
    public class AllowUploadSpecialFilesOnlyAttribute : ActionFilterAttribute
    {
        readonly List<string> _toFilter = new List<string>();
        readonly string _extensionsWhiteList;
        public AllowUploadSpecialFilesOnlyAttribute(string extensionsWhiteList)
        {
            if (string.IsNullOrWhiteSpace(extensionsWhiteList))
                throw new ArgumentNullException("extensionsWhiteList");

            _extensionsWhiteList = extensionsWhiteList;
            var extensions = extensionsWhiteList.Split(',');
            foreach (var ext in extensions.Where(ext => !string.IsNullOrWhiteSpace(ext)))
            {
                _toFilter.Add(ext.ToLowerInvariant().Trim());
            }
        }

        bool canUpload(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName)) return false;

            var ext = Path.GetExtension(fileName.ToLowerInvariant());
            return _toFilter.Contains(ext);
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var files = filterContext.HttpContext.Request.Files;
            foreach (string file in files)
            {
                var postedFile = files[file];
                if (postedFile == null || postedFile.ContentLength == 0) continue;

                if (!canUpload(postedFile.FileName))
                    throw new InvalidOperationException(
                        string.Format("You are not allowed to upload {0} file. Please upload only these files: {1}.",
                                        Path.GetFileName(postedFile.FileName), 
                                        _extensionsWhiteList));
            }

            base.OnActionExecuting(filterContext);
        }
    }
}

توضیحات کدهای فوق:
برای تهیه فیلتر محدود سازی نوع فایل‌های قابل ارسال به سرور، با ارث بری از ActionFilterAttribute شروع خواهیم کرد. سپس با تحریف متد OnActionExecuting آن، توسط filterContext.HttpContext.Request.Files می‌توان به کلیه فایل‌های درحال ارسال به سرور در طی درخواست جاری، دسترسی یافت.
به این ترتیب از طریق مقدار خاصیت postedFile.FileName می‌توان به پسوند فایل در حال ارسال رسید و بر این اساس امکان ارسال فایل‌های غیرمجاز را در نیمه راه با صدور یک استثناء سلب کرد.

برای استفاده از این فیلتر سفارشی تهیه شده نیز می‌توان به نحو زیر عمل کرد:
[AllowUploadSpecialFilesOnly(".jpg,.gif,.png")]
public ActionResult ImageUpload(HttpPostedFileBase file)
در اینجا پسوند فایل‌های مجاز قابل ارسال، توسط یک کاما از هم جدا خواهند شد.


یک نکته تکمیلی:
اگر کاربر قرار است تنها تصویر ارسال کند، بررسی پسوند فایل لازم است اما کافی نیست. برای این منظور می‌توان از کلاس Image واقع شده در فضای نام System.Drawing نیز کمک گرفت:
public static bool IsImageFile(HttpPostedFileBase photoFile)
{
    using (var img = Image.FromStream(photoFile.InputStream))
    {
         return img.Width > 0;
    }
}
در اینجا اگر فایل ارسالی تصویر نباشد، به صورت خودکار یک استثناء صادر خواهد شد.
 
مطالب
پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 2#
در ادامه مطلب پیاده سازی پروژه نقاشی (Paint) به صورت شی گرا 1# به تشریح مابقی کلاس‌های برنامه می‌پردازیم.

با توجه به تجزیه و تحلیل انجام شده تمامی اشیا از کلاس پایه به نام Shape ارث بری دارند حال به توضیح کد‌های این کلاس می‌پردازیم. (به دلیل اینکه توضیحات این کلاس در دو پست نوشته خواهد شد برای این کلاس‌ها از partial class استفاده شده است)
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Net;
 
namespace PWS.ObjectOrientedPaint.Models
{
    /// <summary>
    /// Shape (Base Class)
    /// </summary>
    public abstract partial class Shape
    {
        #region Fields (1)

        private Brush _backgroundBrush;

        #endregion Fields

        #region Properties (16)

        /// <summary>
        /// Gets or sets the brush.
        /// </summary>
        /// <value>
        /// The brush.
        /// </value>
        public Brush BackgroundBrush
        {
            get { return _backgroundBrush ?? (_backgroundBrush = new SolidBrush(BackgroundColor)); }
            private set
            {
                _backgroundBrush = value ?? new SolidBrush(BackgroundColor);
            }
        }

        /// <summary>
        /// Gets or sets the color of the background.
        /// </summary>
        /// <value>
        /// The color of the background.
        /// </value>
        public Color BackgroundColor { get; set; }

        /// <summary>
        /// Gets or sets the end point.
        /// </summary>
        /// <value>
        /// The end point.
        /// </value>
        public PointF EndPoint { get; set; }

        /// <summary>
        /// Gets or sets the color of the fore.
        /// </summary>
        /// <value>
        /// The color of the fore.
        /// </value>
        public Color ForeColor { get; set; }

        /// <summary>
        /// Gets or sets the height.
        /// </summary>
        /// <value>
        /// The height.
        /// </value>
        public float Height
        {
            get
            {
                return Math.Abs(StartPoint.Y - EndPoint.Y);
            }
            set
            {
                if (value > 0)
                    EndPoint = new PointF(EndPoint.X, StartPoint.Y + value);
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is fill.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is fill; otherwise, <c>false</c>.
        /// </value>
        public bool IsFill { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is selected.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance is selected; otherwise, <c>false</c>.
        /// </value>
        public bool IsSelected { get; set; }

        /// <summary>
        /// Gets or sets my pen.
        /// </summary>
        /// <value>
        /// My pen.
        /// </value>
        public Pen Pen
        {
            get
            {
                return new Pen(ForeColor, Thickness);
            }
        }

        /// <summary>
        /// Gets or sets the type of the shape.
        /// </summary>
        /// <value>
        /// The type of the shape.
        /// </value>
        public ShapeType ShapeType { get; protected set; }

        /// <summary>
        /// Gets the size.
        /// </summary>
        /// <value>
        /// The size.
        /// </value>
        public SizeF Size
        {
            get
            {
                return new SizeF(Width, Height);
            }
        }

        /// <summary>
        /// Gets or sets the start point.
        /// </summary>
        /// <value>
        /// The start point.
        /// </value>
        public PointF StartPoint { get; set; }

        /// <summary>
        /// Gets or sets the thickness.
        /// </summary>
        /// <value>
        /// The thickness.
        /// </value>
        public byte Thickness { get; set; }

        /// <summary>
        /// Gets or sets the width.
        /// </summary>
        /// <value>
        /// The width.
        /// </value>
        public float Width
        {
            get
            {
                return Math.Abs(StartPoint.X - EndPoint.X);
            }
            set
            {
                if (value > 0)
                    EndPoint = new PointF(StartPoint.X + value, EndPoint.Y);
            }
        }

        /// <summary>
        /// Gets or sets the X.
        /// </summary>
        /// <value>
        /// The X.
        /// </value>
        public float X
        {
            get
            {
                return StartPoint.X;
            }
            set
            {
                if (value > 0)
                    StartPoint = new PointF(value, StartPoint.Y);
            }
        }

        /// <summary>
        /// Gets or sets the Y.
        /// </summary>
        /// <value>
        /// The Y.
        /// </value>
        public float Y
        {
            get
            {
                return StartPoint.Y;
            }
            set
            {
                if (value > 0)
                    StartPoint = new PointF(StartPoint.X, value);
            }
        }

        /// <summary>
        /// Gets or sets the index of the Z.
        /// </summary>
        /// <value>
        /// The index of the Z.
        /// </value>
        public int Zindex { get; set; }

        #endregion Properties
        }
}


ابتدا به تشریح خصوصیات کلاس می‌پردازیم:
خصوصیات:
  • BackgroundColor: در صورتی که شی مورد نظر به صورت توپررسم شود، این خاصیت رنگ پس زمینه شی را مشخص می‌کند.
  • BackgroundBrush: خاصیتی است که با توجه به خاصیت BackgroundColor یک الگوی پر کردن  زمینه شی می‌سازد.
  • StartPoint: نقطه شروع شی را در خود نگهداری می‌کند.
  • EndPoint: نقطه انتهای شی را در خود نگهداری می‌کند. (قبلا گفته شد که هر شی را در صورتی که در یک مستطیل فرض کنیم یک نقطه شروع و یک نقطه پایان دارد)
  • ForeColor: رنگ قلم ترسیم شی مورد نظر را تعیین می‌کند.
  • Height: ارتفاع شی مورد نظر را تعیین می‌کند ( این خصوصیت اختلاف عمودی StartPoint.Y و EndPoint.Y را محاسبه می‌کند و در زمان مقدار دهی EndPoint جدیدی ایجاد می‌کند).
  • Width: عرض شی مورد نظر را تعیین می‌کند ( این خصوصیت اختلاف افقیStartPoint.X و EndPoint.X را محاسبه می‌کند و در زمان مقدار دهی EndPoint جدیدی ایجاد می‌کند).
  • IsFill: این خصوصیت تعیین کننده توپر و یا توخالی بودن شی است.
  • IsSelected: این خاصیت تعیین می‌کند که آیا شی انتخاب شده است یا خیر (در زمان انتخاب شی چهار مربع کوچک روی شی رسم می‌شود).
  • Pen: قلم خط ترسیم شی را مشخص می‌کند. (قلم با ضخامت دلخواه)
  • ShapeType: این خصوصیت نوع شی را مشخص می‌کند (این خاصیت بیشتر برای زمان پیش نمایش ترسیم شی در زمان اجراست البته به نظر خودم اضافه هست اما راه بهتری به ذهنم نرسید)
  • Size: با استفاده از خصوصیات Height و Width ایجاد شده و تعیین کننده Size شی است.
  • Thickness: ضخامت خط ترسیمی شی را مشخص می‌کند، این خاصیت در خصوصیت Pen استفاده شده است.
  • X: مقدار افقی نقطه شروع شی را تعیین می‌کند در واقع StartPoint.X را برمی‌گرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده می‌توان آن را ننوشت).
  • Y: مقدار عمودی نقطه شروع شی را تعیین می‌کند در واقع StartPoint.Y را برمی‌گرداند (این خاصیت اضافی بوده و جهت راحتی کار استفاده شده می‌توان آن را ننوشت).
  • Zindex: در زمان ترسیم اشیا ممکن است اشیا روی هم ترسیم شوند، در واقع Zindex تعیین کننده عمق شی روی بوم گرافیکی است.

در پست بعدی به توضیح متدهای این کلاس می‌پردازیم.
مطالب
MVVM و امکان استفاده از یک وهله از ViewModel جهت چند View مرتبط

عموما هنگام طراحی یک View، خیلی زود به حجم انبوهی از کدهای XAML خواهیم رسید. در ادامه بررسی خواهیم کرد که چطور می‌توان یک View را به چندین View خرد کرد، بدون اینکه نیازی باشد تا از چندین ViewModel (یا همان code behind عاری از ارجاعات بصری سابق قرار گرفته در یک پروژه جدای دیگر) استفاده شود و تمام این View های خرد شده هم تنها از یک وهله از ViewModel ایی خاص استفاده کنند و با اطلاعاتی یکپارچه سروکار داشته باشند؛ یا در عمل یکپارچه کار کنند.
این مشکل از جایی شروع می‌شود که مثلا خرد کردن یک user control به چند یوزر کنترل، یعنی کار کردن با چند وهله از اشیایی متفاوت. هر چند نهایتا تمام این‌ها قرار است در یک صفحه در کنار هم قرار گیرند اما در عمل از هم کاملا مجزا هستند و اگر به ازای هر کدام یکبار ViewModel را وهله سازی کنیم، به مشکل برخواهیم خورد؛ چون هر وهله نسبت به وهله‌ای دیگر ایزوله است. اگر در یکی Name مثلا Test بود در دیگری ممکن است مقدار پیش فرض نال را داشته باشد؛ چون با چند وهله از یک کلاس، در یک فرم نهایی سروکار خواهیم داشت.

ابتدا Model و ViewModel ساده زیر را در نظر بگیرید:
using System.ComponentModel;

namespace SplittingViewsInMvvm.Models
{
public class GuiModel : INotifyPropertyChanged
{
string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
raisePropertyChanged("Name");
}
}

string _lastName;
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
raisePropertyChanged("LastName");
}
}

#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
void raisePropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler == null) return;
handler(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

using SplittingViewsInMvvm.Models;

namespace SplittingViewsInMvvm.ViewModels
{
public class MainViewModel
{
public GuiModel GuiModelData { set; get; }

public MainViewModel()
{
GuiModelData = new GuiModel();
GuiModelData.Name = "Name";
GuiModelData.LastName = "LastName";
}
}
}

سپس View زیر هم از این اطلاعات استفاده خواهد کرد:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</StackPanel>
</UserControl>

اکنون فرض کنید که می‌خواهیم Group 1 و Group 2 را جهت مدیریت ساده‌تر View اصلی در دو user control مجزا قرار دهیم؛ مثلا:

<UserControl x:Class="SplittingViewsInMvvm.Views.Group1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 1">
<TextBlock Text="{Binding GuiModelData.Name}" />
</GroupBox>
</Grid>
</UserControl>
و
<UserControl x:Class="SplittingViewsInMvvm.Views.Group2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<GroupBox Margin="2" Header="Group 2">
<TextBlock Text="{Binding GuiModelData.LastName}" />
</GroupBox>
</Grid>
</UserControl>

اکنون اگر این دو را مجددا در همان View اصلی ساده شده قبلی قرار دهیم (بدون اینکه در هر user control به صورت جداگانه data context را تنظیم کنیم):
<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 />
<V:Group2 />
</StackPanel>
</UserControl>

باز هم .... برنامه همانند سابق کار خواهد کرد و ViewModel وهله سازی شده در user control فوق به صورت یکسانی در اختیار هر دو View اضافه شده قرار می‌گیرد و نهایتا یک View یکپارچه را در زمان اجرا می‌توان مورد استفاده قرار داد. علت هم بر می‌گردد به مقدار دهی خودکار DataContext هر View اضافه شده به بالاترین DataContext موجود در Visual tree که ذکر آن الزامی نیست:

<UserControl x:Class="SplittingViewsInMvvm.Views.Main"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:V="clr-namespace:SplittingViewsInMvvm.Views"
xmlns:VM="clr-namespace:SplittingViewsInMvvm.ViewModels"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<VM:MainViewModel x:Key="vmMainViewModel" />
</UserControl.Resources>
<StackPanel DataContext="{Binding Source={StaticResource vmMainViewModel}}">
<V:Group1 DataContext="{Binding}" />
<V:Group2 DataContext="{Binding}"/>
</StackPanel>
</UserControl>


بنابراین به صورت خلاصه زمانیکه از MVVM استفاده ‌می‌کنید لازم نیست کار خاصی را جهت خرد کردن یک View به چند Sub View انجام دهید! فقط این‌ها را در چند User control جدا کنید و بعد مجددا به کمک فضای نامی که تعریف خواهد (مثلا V در اینجا) در همان View اصلی تعریف کنید. بدون هیچ تغییر خاصی باز هم برنامه همانند سابق کار خواهد کرد.


مطالب
EF Code First #14

ردیابی تغییرات در EF Code first

EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیت‌های تحت کنترل خود کمک می‌گیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیت‌ها از بانک اطلاعاتی جهت اطمینان از به روز بودن آن‌ها تدارک دیده شده است. ساده‌ترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry می‌باشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیت‌هایی مانند Unchanged یا Modified را می‌توان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز می‌باشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز می‌گرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه می‌دهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues می‌توان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API می‌توان مثال زد که در ادامه به بررسی آن‌ها خواهیم پرداخت.


کلاس‌های مدل مثال جاری

در اینجا یک رابطه many-to-one بین جدول هزینه‌های اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:

using System;

namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }

public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }

public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}

using System;

namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }

public virtual Payee Payee { get; set; }
}
}

using System.Collections.Generic;

namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }

public virtual ICollection<Bill> Bills { set; get; }
}
}


به علاوه همانطور که ملاحظه می‌کنید، این کلاس‌ها از یک abstract class به نام BaseEntity مشتق شده‌اند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاس‌های برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاس‌ها در یک جدول نشود، تنها کافی است آن‌را به عنوان DbSet معرفی نکنیم و یا می‌توان از متد Ignore نیز استفاده کرد:

using System.Data.Entity;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();

base.OnModelCreating(modelBuilder);
}
}
}



الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود

در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API می‌باشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ داده‌اند که هنوز در Context جاری کلاینت منعکس نشده‌اند:

using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;

namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());

using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);

db.SaveChanges();
}

using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";

db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);

db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);

db.SaveChanges();
}
}
}
}

در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه می‌شوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده می‌شود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand می‌باشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.



ب) یکسان سازی ی و ک اطلاعات رشته‌ای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی

یکی از الزامات برنامه‌های فارسی، یکسان سازی ی و ک دریافتی از کاربر است. برای این منظور باید پیش از فراخوانی متد SaveChanges نهایی،‌ مقادیر رشته‌ای کلیه موجودیت‌ها را یافته و به روز رسانی کرد:

using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;

namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;

case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}

public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}

private void applyCorrectYeKe()
{
//پیدا کردن موجودیت‌های تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

foreach (var item in changedEntities)
{
if (item.Entity == null) continue;

//یافتن خواص قابل تنظیم و رشته‌ای این موجودیت‌ها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));

var pr = new PropertyReflector();

//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}

private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;

case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}


اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که می‌توان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه می‌کنید.
در اینجا کار با تحریف متد SaveChanges شروع می‌شود. سپس در متد applyCorrectYeKe کلیه موجودیت‌های تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده‌ باشند، یافت شده و سپس خواص رشته‌ای آن‌ها جهت یکسانی سازی ی و ک، بررسی می‌شوند.


ج) ساده‌تر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت

در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker می‌توان به موجودیت‌هایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آن‌ها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز می‌شوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.


د) پیاده سازی قابلیت لغو تغییرات در برنامه

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



مطالب
روش ذخیره‌ی لاگ‌های ILogger در پایگاه داده در Blazor

مقدمه

همانطور که می‌دانید، Blazor دارای یک سیستم لاگ گیری توکار است که می‌توان از آن توسط تزریق ILogger در کامپوننت‌ها بهره برد. این سیستم لاگ گیری در زمان توسعه‌ی نرم افزار، در قالب یک کنسول، لاگ‌ها را به توسعه دهنده نشان می‌دهد. اما پس از تولید و پابلیش اپلیکیشن، دیگر این کنسول وجود ندارد. برای ذخیره‌ی لاگ‌ها در یک فایل متنی بر روی سرور هاست، می‌توان از Serilog بهره برد که روش آن در اینجا  توضیح داده شده است. حال اگر بخواهیم این لاگ‌ها را در یک پایگاه داده ذخیره کنیم چطور؟

ایجاد کلاس لاگ

برای این منظور ابتدا کلاسی را برای ذخیره‌ی لاگ‌ها در پایگاه داده به شکل زیر ایجاد می‌نماییم:
public class DBLog
    {
        public int  DBLogId { get; set; }
        public string? LogLevel { get; set; }
        public string? EventName { get; set; }
        public string? Message { get; set; }
        public string? StackTrace { get; set; }
        public DateTime CreatedDate { get; set; }=DateTime.Now;
    }


ایجاد دیتابیس لاگر

کلاس DBLogger از اینترفیس ILogger ارث بری می‌کند و دارای سه متد است که مهمترین آنها متد Log می‌باشد که درحقیقت با هر بار فراخوانی Logger در برنامه فراخوانی می‌شود. برای مطالعه‌ی بیشتر در رابطه با دو متد دیگر می‌توانید به اینجا مراجعه نمایید.
public class DBLogger:ILogger

    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DBLogger(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }


        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = exception?.Message,
                StackTrace=exception?.StackTrace                
            }; 
            _dbContext.DBLogs.Add(dblLogItem);
            _dbContext.SaveChanges();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }
    }


ایجاد یک لاگ پروایدر سفارشی

حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.

public class DbLoggerProvider:ILoggerProvider
    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new D‌BLogger(_dbContext);
        }

        public void Dispose()
        {
        }
    }  

همانطور که ملاحظه می‌نمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کرده‌است که دارای متد CreateLogger می‌باشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد می‌کند. در سازنده‌ی این کلاس، DatabaseContext را مقدار دهی نموده‌ایم تا آنرا به کلاس DBLogger ارسال نماییم.

در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider  ) را صدا بزنیم.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            .
            .
            .
            #region CustomLogProvider
            var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
            var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
            loggerFactory.AddProvider(new DbLoggerProvider(appDbContext));
            #endregion
            .
            .
            .
در اینجا ابتدا DBContext پروژه را گرفته سپس DbLoggerProvider   را صدا زده و مقدار DBContext را به آن پاس می‌دهیم.


مشکل!

منطق کدهای بالا کاملا صحیح می‌باشد، اما با اجرای یک اپلیکیشن واقعی، در ابتدای کار اینقدر تعداد فراخوانی ثبت لاگ‌ها در پایگاه داده بالا می‌رود که اپلیکیشن هنگ می‌کند. برای حل این مشکل باید یک صف همزمانی برای ثبت لاگ‌ها تشکیل شود. برای این منظور من از این مطلب پروژه‌ی DNTIdentity بهره بردم. بنابراین باید پروایدر را به شکل زیر تصحیح کنیم:
public class DbLoggerProvider:ILoggerProvider
    {
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private readonly IList<DBLog> _currentBatch = new List<DBLog>();
        private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);

        private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());

        private readonly Task _outputTask;
        private readonly ApplicationDbContext _dbContext;
        private bool _isDisposed;

        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _outputTask = Task.Run(ProcessLogQueue);
        }
        public ILogger CreateLogger(string categoryName)
        {
            return new DBLogger(this,categoryName);
        }
        private async Task ProcessLogQueue()
        {
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                while (_messageQueue.TryTake(out var message))
                {
                    try
                    {
                        _currentBatch.Add(message);
                    }
                    catch
                    {
                        //cancellation token canceled or CompleteAdding called
                    }
                }

                await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
                _currentBatch.Clear();

                await Task.Delay(_interval, _cancellationTokenSource.Token);
            }
        }
        internal void AddLogItem(DBLog appLogItem)
        {
            if (!_messageQueue.IsAddingCompleted)
            {
                _messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
            }
        }
        private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
        {
            try
            {
                if (!items.Any())
                {
                    return;
                }

                // We need a separate context for the logger to call its SaveChanges several times,
                // without using the current request's context and changing its internal state.
                foreach (var item in items)
                {
                    var addedEntry = _dbContext.DbLogs.Add(item);
                }

                await _dbContext.SaveChangesAsync(cancellationToken);

            }
            catch
            {
                // don't throw exceptions from logger
            }
        }

        [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
            Justification = "don't throw exceptions from logger")]
        private void Stop()
        {
            _cancellationTokenSource.Cancel();
            _messageQueue.CompleteAdding();

            try
            {
                _outputTask.Wait(_interval);
            }
            catch
            {
                // don't throw exceptions from logger
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        Stop();
                        _messageQueue.Dispose();
                        _cancellationTokenSource.Dispose();
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }  
همچنین دیتابیس لاگر نیز به شکل زیر تغییر خواهد کرد:
public class DBLogger:ILogger
    {
        private readonly LogLevel _minLevel;
        private readonly DbLoggerProvider _loggerProvider;
        private readonly string _categoryName;

        public DBLogger(
            DbLoggerProvider loggerProvider,
            string categoryName
        )
        {
            _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
            _categoryName= categoryName;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return new NoopDisposable();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _minLevel;
        }

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = formatter(state, exception);

            if (exception != null)
            {
                message = $"{message}{Environment.NewLine}{exception}";
            }

            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = $"{_categoryName}{Environment.NewLine}{message}",
                StackTrace=exception?.StackTrace
            };
            _loggerProvider.AddLogItem(dblLogItem);
        }

      

        private class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
            }
        }
    }  
خلاصه تغییرات اینگونه‌است که بجای ذخیره لاگ‌ها در دیتابیس در داخل کلاس DBLogger، آنها را در یک صف همزمانی اضافه می‌کنیم و پس از اتمام پروسه، ذخیره سازی لاگ‌ها را در لاگ پروایدر انجام می‌دهیم.
توسط ترکیب روش توضیح داده شده در این مقاله با مدیریت استثناءها در Blazor Server - قسمت دوم، علاوه برلاگ‌های معمولی، می‌توان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.