مطالب
CSS پویا در ASP.NET MVC
سناریو هایی وجود دارد که نیاز است مشتری ، خود شیوه نامه هایی (CSS) را برای قسمت‌های مختلف سایت انتخاب کند. برای مثال تنظیماتی را برای منوی سایت در نظر گرفته ایم که مشتری بتواند رنگ و قلم و ... را متناسب با سلیقه‌ی خود تغییر دهد و یا یک قسمت کلی برای اعمال شیوه نامه‌ها به سایت ایجاد کرده ایم که در همه‌ی قسمت‌های سایت اعمال شود. بدین شکل در صورتی که مشتری، اطلاعات اندکی هم در مورد CSS داشته باشد میتواند ظاهر سایت خود را به آسانی تغییر دهد و تا حدودی بار را از روی دوش پشتیبان سایت بر میدارد.
و برای همه‌ی این‌ها نیاز است تا فیلدی در دیتابیس برای ذخیره‌ی شیوه نامه‌های مشتری ایجاد شود و یا در یک فایل متنی ذخیره شود که بسته به سیاست برنامه نویس دارد.
در این مطلب تصمیم داریم این سناریو را به صورت ساده در یک پروژه ASP.NET MVC پیاده سازی کنیم.
ایتدا یک پروژه از نوع ASP.NET MVC 4 ایجاد میکنیم. سپس قطعه کد زیر را به فایل  Layout.cshtml_ موجود در مسیر Views/Shared به صورت زیر اضافه میکنیم :
<head>
    @*سایر شیوه نامه‌ها و اسکریپت ها*@
    @RenderSection("styles", required: false)
</head>
RenderSection این امکان را میدهد که ما بتوانیم شیوه نامه‌ی دیگری را در صفحات دیگر به MasterPage خود تزریق کنیم.
سپس کنترلری به نام Home ایجاد میکنیم :
namespace DynamicCssExample.Controllers
{
    public class HomeController : Controller
    {

        public ActionResult Index()
        {
            return View();
        }

        public string GetStyle()
        {
            Response.ContentType = "text/css";
            //در این قسمت میتوانیم به دیتابیس متصل شویم و شیوه نامه‌ی مورد نظر را واکشی کنیم و بازگشت دهیم
            return "h2{color:yellow}";
        }

    }
}
در اینجا یک متد به نام GetStyle داریم که وظیفه‌ی بازگشت یک رشته را بر عهده دارد و نوع این مقدار بازگشتی از نوع text/css است و میتواند شیوه نامه هایی که در دیتابیس و یا یک فایل متنی ذخیره کرده ایم را واکشی و به استریم ارسال کند.
در انتها برای استفاده از این متد کافی است در View مربوطه بدین شکل عمل کنیم :
@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section styles
{
    <link rel="stylesheet" href="@Url.Action("GetStyle", "Home")" type="text/css"/>
}

<h2>Index</h2>
در سکشن Style لینک شیوه نامه ای تعریف کرده ایم که ویژگی href آن به اکشن GetStyle در کنترلر Home اشاره میکند و این اکشن نیز محتوای شیوه نامه را برگشت میدهد.
مطالب
آموزش فریم ورک Vuetify قسمت اول - نصب و بررسی ساختار grid؛ بخش اول
برای طراحی ui برنامه‌هایی که با فریم ورک vue.js توسعه داده می‌شوند، داشتن یک css framework مناسب، جهت زیبا سازی برنامه، یکی از انتخابهای مهم می‌باشد. در این سری از آموزش‌ها، نحوه‌ی بکارگیری فریم ورک Vuetify را یاد خواهیم گرفت.

چرا vuetify ؟

دلایل زیادی برای استفاده از این framework وجود دارد که از جمله آنها می‌توان به مواردی از قبیل رابط کاربری خوب برای طرح بندی صفحه برنامه،  پشتیبانی از تمام مرورگرها، پشتیبانی از RTL (راست به چپ کردن صفحه)، پشتیبانی از cli3  و موارد دیگر می‌توان اشاره نمود. 


روش نصب vuetify

پس از ایجاد یک برنامه Vuejs  با دستور زیر، فریم ورک Vuetify را اضافه می‌کنیم. 
vue add vuetify


ساختار grid

grid  برای طرح بندی، یا بخش بندی محتوای برنامه استفاده می‌شود .vuetify همانند bootstrap، از سیستم بخش بندی 12 تایی برای تقسیم بندی صفحه استفاده می‌کند. در این روش ما به 5 حالت مختلف می‌توانیم صفحه را بخش بندی کنیم:


طریقه‌ی استفاده

ساختار برنامه‌ی ما شامل یک سری از کامپوننت‌های از پیش تعیین شده برای راحتی و سادگی کار می‌باشد که در زیر به آنها اشاره شده‌است. 

ساختار برنامه ما باید دارای یک v-container باشد تا به درستی کار کند. این کامپوننت در بر گیرنده‌ی تمام صفحه‌ی برنامه است. هر کامپوننت می‌تواند تنظیمات خاص خود را داشته باشد.

برای مثال در کد پایین، تنظیم fluid باعث می‌شود تا کامپوننت v-container تمام عرض صفحه‌ی ما را در بر بگیرد.

<v-container fluid></v-container>

کامپوننت v-layout برای کار با ردیفها مورد استفاده قرار میگیرد که تنظیمات مخصوص به خود را دارد.

برای بخش بندی هر ستون در صفحه می‌توان از کامپوننت v-flex استفاده کرد.

نکته: در توضیح کد پایین، در قسمت تعریف v-layout به وسیله row مشخص میکنیم که می‌خواهیم یک ردیف را ایجاد کنیم و در قسمت تعریف v-flex به وسیله md6 مشخص می‌کنیم که 6 خانه از 12 خانه موجود در ردیف را می‌خواهیم در اختیار داشته باشیم:

<v-container>
  <v-layout row>
    <v-flex md6>
      ...
    </v-flex>
    <v-flex md6>
      ...
    </v-flex>
  </v-layout>
</v-container>


نتیجه‌ی قطعه کد بالا بدین ترتیب است:

Two elements in row

هر کدام از کامپوننت‌های گفته شده، properties مختص به خود را دارد که در برخی موارد مشابه یکدیگر هستند. این properties مشابه flexbox در css عمل میکنند.
نکته: تمامی این property‌ها از نوع داده boolean بوده و مقدار اولیه آنها false می‌باشد. 

breakpoint‌ها : 
به وسیله breakpoint‌ها می‌توانیم مشخص کنیم که هر المان درون هر مدیا (موبایل، تبلت، کامپیوتر و ...) به چه صورتی نمایش داده شود. در حالت کلی پنج نوع breakpoint وجود دارند که به ترتیب شامل:
xs جهت نمایش درون دستگاه‌هایی است که اندازه‌ی صفحه‌ی آنها از 600 پیکسل کمتر باشد.
sm جهت نمایش درون دستگاه‌هایی است که اندازه‌ی آنها از 600 پیکسل بزرگتر و از 960 پیکسل کوچکتر باشد.
md جهت نمایش درون دستگاه‌هایی است که اندازه‌ی آنها از 960 پیکسل بزرگتر و از 1264 پیکسل کوچکتر باشد.
lg جهت نمایش درون دستگاه‌هایی است که اندازه‌ی آنها از 1264 پیکسل بزرگتر و از 1904 پیکسل کوچکتر باشد.
xl جهت نمایش درون دستگاه‌هایی است که اندازه‌ی آنها بیش از 1904 پیکسل باشد.

هر یک از این breakpointها می‌توانند به وسیله‌ی کامپوننت v-flex، حاوی مقداری بین 1 تا 12 باشند تا درون هر مدیا به صورت مشخصی نمایش داده شوند.
نکته: ما میتوانیم برای هر کدام از این مدیا‌ها به صورت جداگانه این تنظیمات را اعمال کنیم.
نکته: اگر برای کوچکترین حالت، یعنی xs، یک مقدار را مشخص کنیم و برای حالت‌های دیگر مقداری را تنظیم نکنیم، به این معنا است که حالتهای دیگر نیز با مقداری که برای xs تنظیم کرده‌ایم، تنظیم می‌شوند.

برای مثال: در این قطعه کد ما برای xs، مقدار 6 و برای sm، مقدار 5 را در نظر گرفته‌ایم. یعنی در دستگاه‌های Extra small، این پارامتر تعداد 6 خانه از 12 خانه در دسترس را اشغال میکند؛ اما درون دستگاه‌های small، تعداد 5 خانه از 12 خانه را اشغال می‌نماید. باید بدانیم به صورت خودکار برای md  وlg ،xl نیز مقدار 5 در نظر گرفته می‌شود. 
<v-flex xs6 sm5>
</v-flex>

به این مثال توجه کنید: در این مثال ما برای مدیاهای xs، مقدار 6 و برای مدیاهای md، مقدار 8 را در نظر گرفته‌ایم. نکته قابل توجه اینجاست که در چنین حالتی برای مدیهای sm، مقدار 6 به صورت خودکار در نظر گرفته میشود و برای مدیاهای lg و xl نیز به صورت خودکار مقدار 8 در نظر گرفته می‌شوند.
<v-flex xs5 md8>
</v-flex>

Offset
به وسیله‌ی offset می‌توانیم مشخص کنیم که هر کامپوننت v-flex از چه موقعیتی در v-layout شروع به گرفتن فضا کند. در پایین یک مثال کاربردی آورده شده‌است تا به صورت بهتر کارکرد offset‌ها را درک کنیم. در توضیح کد پایین در خط پنجم، مقدار offset-xs1 در نظر گرفته شده است. بدین معنا که با توجه به بخش بندی 12 تایی که داریم، کلاس offset-xs1 باعث می‌شود برای گرفتن  فضای تعریف شده توسط xs10، ستون اول را نادیده گرفته و از ستون دوم فضا را اشغال کند. همانطور که قبل‌تر نیز گفته شد، به دلیل اینکه برای xs مقدار 10 تنظیم شده‌است، درون تمامی مدیاها v-flex، تعداد 10 خانه از 12 خانه در دسترس را اشغال می‌کند.
<div id="app">
  <v-app id="inspire">
    <v-container grid-list-xl text-xs-center>
      <v-layout row wrap>
        <v-flex xs10 offset-xs1>
          <v-card dark color="purple">
            <v-card-text>xs10 offset-xs1</v-card-text>
          </v-card>
        </v-flex>
        <v-flex xs7 offset-xs5 offset-md2 offset-lg5>
          <v-card dark color="secondary">
            <v-card-text>xs7 offset-(xs5 | md2 | lg5)</v-card-text>
          </v-card>
        </v-flex>
        <v-flex xs12 sm5 md3>
          <v-card dark color="primary">
            <v-card-text>(xs12 | sm5 | md3)</v-card-text>
          </v-card>
        </v-flex>
        <v-flex xs12 sm5 md5 offset-xs0 offset-lg2>
          <v-card dark color="green">
            <v-card-text>(xs12 | sm5 | md5) offset-(xs0 | lg2)</v-card-text>
          </v-card>
        </v-flex>
      </v-layout>
    </v-container>
  </v-app>
</div>

نتیجه‌ی قطعه کد بالا بدین صورت است:


مطالب
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
نظرات مطالب
LocalDB FAQ
اتصال به LocalDB توسط Rider

در تنظیمات Data Source جهت اتصال به LocalDB در Rider این مراحل باید طی شوند:
- پیش از هر کاری دو دستور زیر را اجرا کنید:
C:\>"C:\Program Files\Microsoft SQL Server\140\Tools\Binn\SqlLocalDB.exe" i
MSSQLLocalDB
ProjectsV13
v12.0

C:\>"C:\Program Files\Microsoft SQL Server\140\Tools\Binn\SqlLocalDB.exe" s MSSQLLocalDB
LocalDB instance "MSSQLLocalDB" started.
دستور اول وهله‌های نصب شده را نمایش می‌دهد و دستور دوم اولین وهله را آغاز می‌کند.
خود Rider آغازگر این وهله‌ها نخواهد بود. به همین جهت نیاز است دستی آغاز شوند.


- سپس در صفحه تنظیمات Data Source، نوع Driver را بر روی SQL Server (jTds) قرار دهید.
- پایین صفحه، لینک download missing driver files ظاهر می‌شود. بر روی آن کلیک کنید تا به سرعت کار نصب و راه اندازی درایور کم حجم آن انجام شود.
- اکنون می‌توانید در قسمت URL، گزینه‌ی LocalDB و سپس وهله‌ی MSSQLLocalDB را از لیست Instance انتخاب کنید.
- در آخر بر روی دکمه‌ی Test Connection کلیک کنید. اگر درایور را نصب نکرده باشید، این دکمه قابل انتخاب نخواهد بود.
مطالب
Blazor 5x - قسمت 32 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 2 - ثبت نام،‌ ورود به سیستم و خروج از آن
در قسمت 25، سرویس‌های سمت سرور اعتبارسنجی و احراز هویت مبتنی بر ASP.NET Core Identity را تهیه کردیم. همچنین در قسمت قبل، سرویس‌های سمت کلاینت کار با این Web API Endpoints را توسعه دادیم. در این مطلب، رابط کاربری متصل کننده‌ی بخش‌های سمت کلاینت و سمت سرور را تکمیل خواهیم کرد.


تکمیل فرم ثبت نام کاربران


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/registration"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager


<EditForm Model="UserForRegistration" OnValidSubmit="RegisterUser" class="pt-4">
    <DataAnnotationsValidator />
    <div class="py-4">
        <div class=" row form-group ">
            <div class="col-6 offset-3 ">
                <div class="card border">
                    <div class="card-body px-lg-5 pt-4">
                        <h3 class="col-12 text-success text-center py-2">
                            <strong>Sign Up</strong>
                        </h3>
                        @if (ShowRegistrationErrors)
                        {
                            <div>
                                @foreach (var error in Errors)
                                {
                                    <p class="text-danger text-center">@error</p>
                                }
                            </div>
                        }
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Name" class="form-control" placeholder="Name..." />
                            <ValidationMessage For="(()=>UserForRegistration.Name)" />
                        </div>
                        <div class="py-2">
                            <InputText @bind-Value="UserForRegistration.Email" class="form-control" placeholder="Email..." />
                            <ValidationMessage For="(()=>UserForRegistration.Email)" />
                        </div>
                        <div class="py-2 input-group">
                            <div class="input-group-prepend">
                                <span class="input-group-text"> +1</span>
                            </div>
                            <InputText @bind-Value="UserForRegistration.PhoneNo" class="form-control" placeholder="Phone number..." />
                            <ValidationMessage For="(()=>UserForRegistration.PhoneNo)" />
                        </div>
                        <div class="form-row py-2">
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.Password" type="password" id="password" placeholder="Password..." class="form-control" />
                                <ValidationMessage For="(()=>UserForRegistration.Password)" />
                            </div>
                            <div class="col">
                                <InputText @bind-Value="UserForRegistration.ConfirmPassword" type="password" id="confirm" class="form-control" placeholder="Confirm Password..." />
                                <ValidationMessage For="(()=>UserForRegistration.ConfirmPassword)" />
                            </div>
                        </div>
                        <hr style="background-color:aliceblue" />
                        <div class="py-2">
                            @if (IsProcessing)
                            {
                                <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
                            }
                            else
                            {
                                <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Register</button>
                            }
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</EditForm>

@code{
    UserRequestDTO UserForRegistration = new UserRequestDTO();
    bool IsProcessing;
    bool ShowRegistrationErrors;
    IEnumerable<string> Errors;

    private async Task RegisterUser()
    {
        ShowRegistrationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.RegisterUserAsync(UserForRegistration);
        if (result.IsRegistrationSuccessful)
        {
            IsProcessing = false;
            NavigationManager.NavigateTo("/login");
        }
        else
        {
            IsProcessing = false;
            Errors = result.Errors;
            ShowRegistrationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس UserRequestDTO تشکیل شده‌است که همان شیءای است که اکشن متد ثبت نام سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا روشی را جهت غیرفعال کردن یک دکمه، پس از کلیک بر روی آن مشاهده می‌کنید. می‌توان پس از کلیک بر روی دکمه‌ی ثبت نام، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را برای مثال با ویژگی disabled در صفحه درج کرد و یا حتی آن‌را از صفحه حذف کرد. این روش، یکی از روش‌های جلوگیری از کلیک چندباره‌ی کاربر، بر روی یک دکمه‌است.
- فرم جاری، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد ثبت نام است:


- پس از پایان موفقیت آمیز ثبت نام، کاربر را به سمت فرم لاگین هدایت می‌کنیم.


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


در ادامه کدهای کامل کامپوننت فرم ثبت نام کاربران را مشاهده می‌کنید:
@page "/login"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

<div id="logreg-forms">
    <h1 class="h3 mb-3 pt-3 font-weight-normal text-primary" style="text-align:center;">Sign In</h1>
    <EditForm Model="UserForAuthentication" OnValidSubmit="LoginUser">
        <DataAnnotationsValidator />
        @if (ShowAuthenticationErrors)
        {
            <p class="text-center text-danger">@Errors</p>
        }
        <InputText @bind-Value="UserForAuthentication.UserName" id="email" placeholder="Email..." class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.UserName)"></ValidationMessage>
        <InputText @bind-Value="UserForAuthentication.Password" type="password" placeholder="Password..." id="password" class="form-control mb-2" />
        <ValidationMessage For="(()=>UserForAuthentication.Password)"></ValidationMessage>
        @if (IsProcessing)
        {
            <button type="submit" class="btn btn-success btn-block disabled"><i class="fas fa-sign-in-alt"></i> Please Wait...</button>
        }
        else
        {
            <button type="submit" class="btn btn-success btn-block"><i class="fas fa-sign-in-alt"></i> Sign in</button>
        }
        <a href="/registration" class="btn btn-primary text-white mt-3"><i class="fas fa-user-plus"></i> Register as a new user</a>
    </EditForm>
</div>
@code
{
    AuthenticationDTO UserForAuthentication = new AuthenticationDTO();
    bool IsProcessing = false;
    bool ShowAuthenticationErrors;
    string Errors;
    string ReturnUrl;

    private async Task LoginUser()
    {
        ShowAuthenticationErrors = false;
        IsProcessing = true;
        var result = await AuthenticationService.LoginAsync(UserForAuthentication);
        if (result.IsAuthSuccessful)
        {
            IsProcessing = false;
            var absoluteUri = new Uri(NavigationManager.Uri);
            var queryParam = HttpUtility.ParseQueryString(absoluteUri.Query);
            ReturnUrl = queryParam["returnUrl"];
            if (string.IsNullOrEmpty(ReturnUrl))
            {
                NavigationManager.NavigateTo("/");
            }
            else
            {
                NavigationManager.NavigateTo("/" + ReturnUrl);
            }
        }
        else
        {
            IsProcessing = false;
            Errors = result.ErrorMessage;
            ShowAuthenticationErrors = true;
        }
    }
}
توضیحات:
- مدل این فرم بر اساس AuthenticationDTO تشکیل شده‌است که همان شیءای است که اکشن متد لاگین سمت Web API انتظار دارد.
- در این کامپوننت به کمک سرویس IClientAuthenticationService که آن‌را در قسمت قبل تهیه کردیم، شیء نهایی متصل به فرم، به سمت Web API Endpoint ثبت نام ارسال می‌شود.
- در اینجا نیز همانند فرم ثبت نام، پس از کلیک بر روی دکمه‌ی ورود به سیستم، با true کردن یک فیلد مانند IsProcessing، بلافاصله دکمه‌ی جاری را با ویژگی disabled در صفحه درج کرد‌ه‌ایم تا از کلیک چندباره‌ی کاربر، جلوگیری شود.
- این فرم، خطاهای اعتبارسنجی مخصوص Identity سمت سرور را نیز نمایش می‌دهد که حاصل از ارسال آن‌ها توسط اکشن متد لاگین است:


- پس از پایان موفقیت آمیز ورود به سیستم، یا کاربر را به آدرسی که پیش از این توسط کوئری استرینگ returnUrl مشخص شده، هدایت می‌کنیم و یا به صفحه‌ی اصلی برنامه. همچنین در اینجا Local Storage نیز مقدار دهی شده‌است:


همانطور که مشاهده می‌کنید، مقدار JWT تولید شده‌ی پس از لاگین و همچنین مشخصات کاربر دریافتی از Web Api، جهت استفاده‌های بعدی، در Local Storage مرورگر درج شده‌اند.


تغییر منوی راهبری سایت، بر اساس وضعیت لاگین شخص


تا اینجا قسمت‌های ثبت نام و ورود به سیستم را تکمیل کردیم. در ادامه نیاز داریم تا منوی سایت را هم بر اساس وضعیت اعتبارسنجی شخص، تغییر دهیم. برای مثال اگر شخصی به سیستم وارد شده‌است، باید در منوی سایت، لینک خروج و نام خودش را مشاهده کند و نه مجددا لینک‌های ثبت‌نام و لاگین را. جهت تغییر منوی راهبری سایت، کامپوننت Shared\NavMenu.razor را گشوده و لینک‌های قبلی ثبت‌نام و لاگین را با محتوای زیر جایگزین می‌کنیم:
<AuthorizeView>
    <Authorized>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="#">
             <span class="p-2">
                Hello, @context.User.Identity.Name!
             </span>
          </NavLink>
        </li>
        <li class="nav-item p-0">
          <NavLink class="nav-link" href="logout">
             <span class="p-2">
                Logout
             </span>
          </NavLink>
        </li>
    </Authorized>
    <NotAuthorized>
        <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>
    </NotAuthorized>
</AuthorizeView>
نمونه‌ی چنین منویی را در قسمت 22 نیز مشاهده کرده بودید. AuthorizeView، یکی از کامپوننت‌های استانداردBlazor  است. زمانیکه کاربری به سیستم لاگین کرده باشد، فرگمنت Authorized و در غیر اینصورت قسمت NotAuthorized آن‌را مشاهده خواهید کرد و همانطور که در قسمت قبل نیز عنوان شد، این کامپوننت برای اینکه کار کند، نیاز دارد به اطلاعات AuthenticationState (و یا همان لیستی از User Claims) دسترسی داشته باشد که آن‌را توسط یک AuthenticationStateProvider سفارشی، به سیستم معرفی و توسط کامپوننت CascadingAuthenticationState، به صورت آبشاری در اختیار تمام کامپوننت‌های برنامه قرار دادیم که نمونه‌ای از آن، درج مقدار context.User.Identity.Name در منوی سایت است.


تکمیل قسمت خروج از سیستم

اکنون که لینک logout را در منوی سایت، پس از ورود به سیستم نمایش می‌دهیم، می‌توان کدهای کامپوننت آن‌را (Pages\Authentication\Logout.razor) به صورت زیر تکمیل کرد:
@page "/logout"

@inject IClientAuthenticationService AuthenticationService
@inject NavigationManager NavigationManager

@code
{
    protected async override Task OnInitializedAsync()
    {
        await AuthenticationService.LogoutAsync();
        NavigationManager.NavigateTo("/");
    }
}
در اینجا در ابتدا توسط سرویس IClientAuthenticationService و متد LogoutAsync آن، کلیدهای Local Storage مربوط به JWT و اطلاعات کاربر حذف می‌شوند و سپس کاربر به صفحه‌ی اصلی هدایت خواهد شد.

مشکل! پس از کلیک بر روی logout، هرچند می‌توان مشاهده کرد که اطلاعات Local Storage به درستی حذف شده‌اند، اما ... پس از هدایت به صفحه‌ی اصلی برنامه، هنوز هم لینک logout و نام کاربری شخص نمایان هستند و به نظر هیچ اتفاقی رخ نداده‌است!
علت اینجا است که AuthenticationStateProvider سفارشی را که تهیه کردیم، فاقد اطلاع رسانی تغییر وضعیت است:
namespace BlazorWasm.Client.Services
{
    public class AuthStateProvider : AuthenticationStateProvider
    {
        // ...

        public void NotifyUserLoggedIn(string token)
        {
            var authenticatedUser = new ClaimsPrincipal(
                                        new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType")
                                    );
            var authState = Task.FromResult(new AuthenticationState(authenticatedUser));
            base.NotifyAuthenticationStateChanged(authState);
        }

        public void NotifyUserLogout()
        {
            var authState = Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
            base.NotifyAuthenticationStateChanged(authState);
        }
    }
}
در اینجا نیاز است پس از لاگین و یا خروج شخص، حتما متد NotifyAuthenticationStateChanged کلاس پایه‌ی AuthenticationStateProvider فراخوانی شود تا وضعیت AuthenticationState ای که در اختیار کامپوننت‌ها قرار می‌گیرد نیز تغییر کند. در غیراینصورت login و logout و یا هر تغییری در لیست user claims، به صورت آبشاری در اختیار کامپوننت‌های برنامه قرار نمی‌گیرند. به همین جهت دو متد عمومی NotifyUserLoggedIn و NotifyUserLogout را به AuthStateProvider اضافه می‌کنیم، تا این متدها را در زمان‌های لاگین و خروج از سیستم، در سرویس ClientAuthenticationService، فراخوانی کنیم:
namespace BlazorWasm.Client.Services
{
    public class ClientAuthenticationService : IClientAuthenticationService
    {
        private readonly HttpClient _client;
        private readonly ILocalStorageService _localStorage;
        private readonly AuthenticationStateProvider _authStateProvider;

        public ClientAuthenticationService(
            HttpClient client,
            ILocalStorageService localStorage,
            AuthenticationStateProvider authStateProvider)
        {
            _client = client;
            _localStorage = localStorage;
            _authStateProvider = authStateProvider;
        }

        public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication)
        {
            // ...
            if (response.IsSuccessStatusCode)
            {
                //...
                ((AuthStateProvider)_authStateProvider).NotifyUserLoggedIn(result.Token);

                return new AuthenticationResponseDTO { IsAuthSuccessful = true };
            }
            //...
        }

        public async Task LogoutAsync()
        {
            //...
            ((AuthStateProvider)_authStateProvider).NotifyUserLogout();
        }
    }
}
در اینجا تغییرات لازم اعمالی به سرویس ClientAuthenticationService را مشاهده می‌کنید:
- ابتدا AuthenticationStateProvider به سازنده‌ی کلاس تزریق شده‌است.
- سپس در حین لاگین موفق، متد NotifyUserLoggedIn آن فراخوانی شده‌است.
- در آخر پس از خروج از سیستم، متد NotifyUserLogout فراخوانی شده‌است.

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


محدود کردن دسترسی به صفحات برنامه بر اساس نقش‌های کاربران

پس از ورود کاربر به سیستم و تامین AuthenticationState، اکنون می‌خواهیم تنها این نوع کاربران اعتبارسنجی شده بتوانند جزئیات اتاق‌ها (برای شروع رزرو) و یا صفحه‌ی نمایش نتیجه‌ی پرداخت را مشاهده کنند. البته نمی‌خواهیم صفحه‌ی نمایش لیست اتاق‌ها را محدود کنیم. برای این منظور ویژگی Authorize را به ابتدای تعاریف کامپوننت‌های PaymentResult.razor و RoomDetails.razor، اضافه می‌کنیم:
@attribute [Authorize(Roles = ‍ConstantRoles.Customer)]
که البته در اینجا ذکر فضای نام آن در فایل BlazorWasm.Client\_Imports.razor، ضروری است:
@using Microsoft.AspNetCore.Authorization

با این تعریف، دسترسی به صفحات کامپوننت‌های یاد شده، محدود به کاربرانی می‌شود که دارای نقش Customer هستند. برای معرفی بیش از یک نقش، فقط کافی است لیست نقش‌های مدنظر را که می‌توانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننت‌ها معرفی کرد و نمونه‌ای از آن‌را در مطلب 23 مشاهده کردید.
نکته‌ی مهم: فیلتر Authorize را باید بر روی اکشن متدهای متناظر سمت سرور نیز قرار داد؛ در غیراینصورت تنها نیمی از کار انجام شده‌است و هنوز آزادانه می‌توان با Web API Endpoints موجود کار کرد.


تکمیل مشخصات هویتی شخصی که قرار است اتاقی را رزرو کند

پیشتر در فرم RoomDetails.razor، اطلاعات ابتدایی کاربر را مانند نام او، دریافت می‌کردیم. اکنون با توجه به محدود شدن این کامپوننت به کاربران لاگین کرده، می‌توان اطلاعات کاربر وارد شده‌ی به سیستم را نیز به صورت خودکار بارگذاری و تکمیل کرد:
@page "/hotel-room-details/{Id:int}"

// ...

@code {
     // ...

    protected override async Task OnInitializedAsync()
    {
        try
        {
            HotelBooking.OrderDetails = new RoomOrderDetailsDTO();
            if (Id != null)
            {
                // ...

                if (await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails) != null)
                {
                    var userInfo = await LocalStorage.GetItemAsync<UserDTO>(ConstantKeys.LocalUserDetails);
                    HotelBooking.OrderDetails.UserId = userInfo.Id;
                    HotelBooking.OrderDetails.Name = userInfo.Name;
                    HotelBooking.OrderDetails.Email = userInfo.Email;
                    HotelBooking.OrderDetails.Phone = userInfo.PhoneNo;
                }
            }
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }
در اینجا با توجه به اینکه UserId هم مقدار دهی می‌شود، می‌توان سطر زیر را از ابتدای متد SaveRoomOrderDetailsAsync سرویس ClientRoomOrderDetailsService، حذف کرد:
public async Task<RoomOrderDetailsDTO> SaveRoomOrderDetailsAsync(RoomOrderDetailsDTO details)
{
   // details.UserId = "unknown user!";
به این ترتیب هویت کاربری که کار خرید را انجام می‌دهد، دقیقا مشخص خواهد شد و همچنین پس از بازگشت از صفحه‌ی پرداخت بانکی، اطلاعات او مجددا از Local Storage دریافت و مقدار دهی اولیه می‌شود.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-32.zip
مطالب
راه اندازی StimulSoft Report در ASP.NET MVC
یکی از ارکان لاینفک سیستم‌های سازمانی در هر پلتفرمی، چه وب و چه دسکتاپ و ... گزارش گیری از اطلاعات موجود و جزو ساختار حیاتی آن است. از آنجا که حتی ممکن است این گزارشات در هر دوره نیاز به تغییرات داشته باشند و گزارش‌های پویایی باشند؛ این نیاز احساس می‌شود که از یک برنامه گزارش ساز مناسب بهره ببریم. یکی از گزارش سازهای محبوب به خصوص در ایران که حتی نماینده رسمی آن هم در ایران وجود دارد، گزارش ساز StimulSoft Report است.

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

در این مقاله قصد داریم با نحوه راه ندازی این ابزار در وب (MVC) آشنا شویم که شامل مباحث زیر می‌شود:
  1. استفاده از EF به عنوان منبع داده و ارسال آن‌ها به سمت گزارش ساز
  2. نحوه طراحی فایل MRT و بایند کردن داده‌های اطلاعاتی و ایجاد جدول
  3. استفاده از امکانات فایل خروجی ، چاپ و پیش نمایش و...
  4. بررسی Direction جهت استفاده در محیط‌های فارسی زبان
  5. نحوه ارسال اطلاعات بین دو اکشن متفاوت


طراحی فایل MRT

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

برای اینکه فایل MRT بتواند دیتاهای لازمی را که به آن پاس میدهیم، بخواند و در جای مشخص شده قرار بدهد، باید یک BussinessObject برای آن ایجاد کنیم. بعد از اینکه یک گزارش جدید ایجاد کردید، در سمت راست به قسمت Dictionary بروید و در قسمت BussinessObject گزینه NewBussinessObject را انتخاب کنید. یک نام و نام مستعار که عموما هم یکی است، برای آن انتخاب کنید. در زیر همان پنجره شما می‌توانید ستون‌های اطلاعاتی خود را تعریف کنید. در اینجا من میخواهم اطلاعات یک راننده را به همراه خودروی وی، نشان دهم. برای همین، من دو موجودیت راننده و خودروی راننده را دارم. پس اسم Business Object را DriverReport میگذارم و ستون‌های اطلاعاتی فقط راننده (بدون در نظر گرفتن خودروی وی) را وارد میکنم.

در همین کادر بالا شما میتوانید تصیم بگیرید که آیا میخواهید اطلاعات خودرو را به همراه دیگرستون‌های اطلاعاتی راننده، ایجاد کنید یا اینکه برای خودرو یک نوع مجزا انتخاب کنید. اگر تنها یک خودرو برای راننده باشد، شاید راحت‌تر باشید همانند اطلاعات راننده با آن رفتار کنید. ولی اگر مثلا بخواهید خودروی‌های گذشته راننده را هم جز لیست داشته باشید، بهتر است یک  Business Object جدید متعلق و زیر مجموعه Business Object راننده ایجاد کنید. در اینجا چون تنها یک خودرو است، من آن اطلاعات آن را به همراه راننده، ارسال میکنم. شکل زیر ساختار درختی از گزارش بالاست:

شکل زیر هم یک ساختار دیگر از یک گزارش است که شامل Business object‌های مختلف می‌شود: 

سپس همین فیلدها را به سمت صفحه خالی بکشانید. با دو بار کلیک روی فیلدهای قرار گرفته در صفحه، با نحوه بایند کردن مقادیر آشنا می‌شوید؛ هر فیلدی که قرار است دیتای آن بایند شود، باید به شکل زیر در بخش Expression پنجره باز شده، نوشته شود:

{driverReport.LastName}
در صورتیکه قرار است Business Object به شکل یک لیست ارسال شود؛ مثلا لیست رانندگان یا حتی لیست خودروهایی که یک راننده از گذشته تا کنون داشته است، می‌توانید به جای درگ کردن فیلد به درون صفحه، خود Business Object را درگ کنید تا از روی آن یک جدول درست شود و با ارسال یک لیست به سمت آن و به ازای هر آیتم از این لیست یک سطر داشته باشید.

در دیکشنری همچنین انواع دیگری از فیلدها نیز به چشم میخورد:
متغیرها: این نوع فیلد یک متغیر است که به طور جداگانه میتواند مقداردهی شود و از آن بیشتر برای ارسال داده‌های تکی چون تصاویر، تاریخ شمسی و ... میتوان استفاده کرد.
متغیرهای سیستمی: این نوع متغیرها توسط خود گزارش ساز به طور مستقیم پر می‌شوند که شامل شماره صفحه، تاریخ و زمان، تعداد صفحات، مقادیر دو ارزشی (آیا صفحه آخر گزارش است؟) و ... می‌شود.
توابع: گزارش ساز شامل یک سری توابع آماده برای اعمال تغییرات بر روی داده‌ها میباشد که در دسته‌های مختلفی چون کار با رشته‌ها، زمان، ریاضیات و... قرار گرفته‌اند.
بعد از تکمیل آن، فایل MRT را ذخیره و در یک دایرکتوری در ساختار پروژه قرار دهید.

راه اندازی گزارش ساز در ASP.Net MVC

اولین کاری که می‌کنیم، ورود سه dll اصلی به پروژه است:

Stimulate.Base

Stimulate.Report

Stimulate.Report.MVC

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

public ActionResult Report(int id)
{
   return View();
}
در ویوی مربوطه کدهای زیر را اضافه می‌کنیم:
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions()
{
   
    Localization = "~/content/reports/fa.xml",
    Actions =
        {
        
            GetReportSnapshot = "LoadReportSnapshot",
            ViewerEvent = "ViewerEvent",
            ExportReport = "ExportReport",
            PrintReport = "PrintReport",
        }
 }

در نسخه‌های دو سال اخیر، استفاده از این Helper تفاوت‌هایی در نحوه استفاده از خصوصیت‌های آن کرده است. در این روش جدید، پراپرتی‌ها دسته بندی شده و برای دسترسی به هر کدام باید به بخش آن مراجعه کنید؛ مثلا پراپرتی‌های Action، در دسته Actions قرار گرفته‌اند یا خصوصیت‌های ظاهری در دسته Appearance، یا گزینه‌های مرتبط با خروجی گرفتن‌ها، در دسته Export قرار گرفته‌اند و الی آخر که در نسخه‌های پیشین، کد بالا را به شکل زیر،  با پیشوند نام دسته می‌نوشتیم:
@Html.Stimulsoft().StiMvcViewer(new StiMvcViewerOptions()
{
   
    Localization = "~/content/reports/fa.xml",
            ActionGetReportSnapshot = "LoadReportSnapshot",
            ActionViewerEvent = "ViewerEvent",
            ActionExportReport = "ExportReport",
            ActionPrintReport = "PrintReport",
            
}
خصوصیت GetReportSnapshot نام یک اکشن متد است که کار ارسال دیتا را به گزارش ساز، انجام میدهد. باقی خصوصیت‌ها را در ادامه بررسی میکنیم. پس متد LoadReportSnapshot را ایجاد می‌کنم و کدهای زیر را در ادامه آن می‌نویسیم:

بعد از آن لازم است دیتا‌ها را از طریق EF خوانده و به یک مدل جدید که بر اساس اطلاعات گزارش شماست و قرار است گزارش شما این پراپرتی‌ها را بشناسد، به طور دستی یا با استفاده یک کتابخانه mapping مثل automapper انتقال دهید. یا حتی می‌توانید مانند کد زیر از ساختاری ناشناس استفاده کنید. در کد زیر، من به صورت تمرینی اطلاعات یک راننده و خودروی او را انتقال میدهم:
var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                Model = "نام خودرو",
                MotorNumber = 415244,
                ProductionYear = 1394,
                Capacity = 4
                
};
اگر اطلاعات خودرو را هم به صورت مجزا BussinessObject ساخته‌اید باید آن را به شکل زیر تعریف کنید ( با فرض اینکه نام BussinessObject در گزارش، با نام car تعریف شده باشد):
var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                car = new
                {
                    Model = "نام خودرو",
                    MotorNumber = 415244,
                    ProductionYear = 1394,
                    Capacity = 4
                }
};
بعد از اینکه دیتاهای لازم را بر اساس فرمت دلخواه خود آماده کردیم، باید آن‌ها را به سمت گزارش ساز ارسال کنیم:
var report = new StiReport();
            report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
در مرحله اول یک وهله از شیء StiReport را می‌سازیم و فایل گزارشی را که در مرحله قبل ساختیم، به آن معرفی میکنیم. سپس داده‌های لازم را به آن انتقال می‌دهیم. پارامتر اول نام BussinessObject اصلی یعنی driverReport را وارد می‌کنیم و پارامتر دوم هم، همان اطلاعات گزارش است. خط بعدی هم یک متغیر است که من در گزارش تعریف کرده‌ام و در اینجا آن را با تاریخ شمسی امروز پر میکنم. توجه داشته باشید که انتقال اطلاعات حتما باید بعد از استفاده از متد Load باشد؛ در غیر اینصورت انتقالی انجام نخواهد شد. اینکه صرفا شما وهله‌ای از شیء StiReport بسازید و مقادیر را بدون ترتیب پر کنید، کفایت نمی‌کند. یعنی ترتیب زیر یک ترتیب کاملا اشتباه است:
var report = new StiReport();
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
           report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
چیزی که بعدا در خروجی می‌بینید، یک صفحه گزارش بدون مقدار است.
پس کد کامل ما برای ایجاد یک گزارش به شکل زیر می‌شود:
 public ActionResult LoadReportSnapshot()
{
  var driver = new
            {
                FirstName = "علی",
                LastName = "یگانه مقدم",
                NationalCode = "12500000000",
                FatherName = "حسین",
                Model = "نام خودرو",
                MotorNumber = 415244,
                ProductionYear = 1394,
                Capacity = 4

            };

            var report = new StiReport();
            report.Load(Server.MapPath("~/Content/Reports/driver.mrt"));
            report.RegBusinessObject("driverReport", driver);
            report.Dictionary.Variables.Add("today", DateTime.Today.ToPersianString(PersianDateTimeFormat.Date));
return StiMvcViewer.GetReportSnapshotResult(HttpContext, report);
}
همه خطوط، همان قبلی هاست که بررسی کردیم؛ بجز خط آخر که یک ActionResult اختصاصی است که در آن نحوه انتقال اطلاعات به گزارش ساز پیاده سازی شده است و تنها کاری که باید بکنیم این است که شیء گزارش ایجاد شده در بالا را به آن پاس دهیم.

اگر دوباره در ویو مربوطه، به سراغ helper برویم می‌بینیم که سه اکشن متد دیگر وجود دارند که در زیر، به ترتیب با نحوه کار آن‌ها و کد اکشن متد آن‌ها اشاره میکنیم:
Viewer Events : این اکشن متد که تنها یک خط ActionResult استاتیک را فراخوانی می‌کند، جهت مدیریت رویدادهای گزارش چون: زوم، صفحه بندی گزارش، خروجی‌ها و چاپ می‌باشد و وجود آن در گزارش از الزامات است.
 public virtual ActionResult ViewerEvent()
 {
            return StiMvcViewer.ViewerEventResult();
 }

PrintReport: برای مدیریت و ارسال گزارشات به دستگاه چاپ می‌باشد. این اطلاعات از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط PrintReportResult آن را دریافت می‌کند.
public virtual ActionResult PrintReport()
        {
            return StiMvcViewer.PrintReportResult(this.HttpContext);
        }

ExportReport: گزارش ساز استیمول به شما اجاز میدهد در فرمت‌های گوناگونی چون xlsx,docx,pptx,pdf,rtf و ... از گزارش خود خروجی بگیرید. اطلاعات گزارش از طریق شی HttpContext به سمت اکشن متد ارسال شده و توسط ExportReportResult  دریافت می‌شود. 
 public virtual ActionResult ExportReport()
        {
            return StiMvcViewer.ExportReportResult(this.HttpContext);
        }
حال اگر برنامه را اجرا کنید، باید گزارشی به شکل زیر نمایش داده شود و مقادیر در جای خود شکل گرفته باشند. ولی مشکلی  که ممکن است این گزارش داشته باشد این است که برای فارسی، حالت راست به چپ را ندارد.


 البته خوشبختانه این مشکل  در حالت پیش نمایش و چاپ و خروجی‌ها دیده نمی‌شود و فقط مختص نمایش روی فرم Html است. برای حل این مشکل ممکن است از گزینه یا پراپرتی RightToLeft، در بخش Appearance موجود در helper استفاده کنید که البته استفاده از آن مانند تصویر بالا، فقط محدود به container گزارش و نوار ابزار آن می‌شود. برای حل این مشکل کافی است کد css زیر را به صفحه گزارش اضافه کنید تا مشکل حل شود:
.stiMvcViewerReportPanel table{
    direction:ltr !important;
}
مجددا گزارش را ایجادکنید تا گزارش را به طور صحیحی مشاهده کنید:

حال حتما پیش خود میگویید که این روش برای اطلاعات ایستا و تمرینی مناسب است و من چگونه باید پارامترهای ارسالی به اکشن متد Report را به اکشن متد LoadReportSnapshot ارسال کنم. برای این منظور استفاده از SessionState‌ها زیاد توصیه شده‌است:
 public virtual ActionResult Report(int id)
        {
            TempData["id"]=id;
            return View();
        }

         public virtual ActionResult LoadReportSnapshot()
        {
            var driverId = (int)TempData ["id"];
              //.....
        }
  ولی این روش مشکلات زیادی را دارد. اول اینکه اگر کاربر چند گزارش جداگانه را پشت سر هم باز کند، به دلیل اینکه گزارش مدتی طول می‌کشد باز شود، همه گزارش‌ها آخرین گزارش درخواستی خواهند بود و دوم اینکه مقداری از حافظه سرور را هم بی جهت اشغال میکند. ولی برای کار با استیمول به هیچ یک از این کارها نیازی نیست، چون خود استیمول به طور خودکار پارامترهای ارسالی را انتقال می‌دهد. یعنی کد باید به شکل زیر نوشته شود:
public virtual ActionResult Report(int id)
        {
    
            return View();
        }

         public virtual ActionResult LoadReportSnapshot(int id)
        {
             //.....
        }
همین مقدار کد برای ارسال پارمترها کفایت میکند و مابقی کار را به stimul بسپارید.

نکته بسیار مهم: گزارش ساز استیمول متاسفانه شامل تنظیم پیش فرض نامناسبی است که عملیات کش را بر روی گزارش‌ها اعمال می‌کند. به عنوان مثال تصور کنید من صفحه گزارش شخصی به نام «وحید نصیری» را باز میکنم و در تب دیگر گزارش شخص دیگری با نام «علی یگانه مقدم» را باز میکنم. حال اگر کاربر به سراغ تب آقای نصیری برود و بخواهد چاپ یا خروجی درخواست کند، اشتباها با گزارش علی یگانه مقدم روبرو خواهد شد که این اتفاق به دلیل کش شدن رخ میدهد. برای غیر فعال کردن این قابلیت پیش فرض، کد زیر را در Helper اضافه کنید:
Server =
    {
        GlobalReportCache = false
    }
مطالب
توسعه برنامه‌های Cross Platform با Xamarin Forms & Bit Framework - قسمت دهم
در قسمت قبلی، به Binding‌ها پرداختیم و در این قسمت قصد بررسی Triggers و موارد پیشرفته‌تر در Bindings را داریم. در مثال‌های قسمت قبل علاوه بر Bind کردن View به View Model، مثالی را داشتیم که در آن View به View بایند شده بود (Text یک Label به Text یک Entry بایند شده بود، به شکلی که هر آنچه که در Entry تایپ می‌شد، در همان لحظه روی Label نیز دیده می‌شد). خب در آن مثال، Text در هر دو کنترل از یک جنس بود (‌string).
اما اگر بخواهیم مثلا زمانی که روی Entry فوکوس  شده، متن آن Bold شود چه؟ در این صورت، IsFocused ای که از جنس bool است، به FontAttributes ای که یک Enum شامل موارد Italic/Bold و ... است، بایند شده‌است. زمانی که دو طرف Binding از یک جنس نباشند، راهکارهایی باید اتخاذ شوند که Trigger یکی از آنهاست. برای پیاده سازی این مثال داریم:
        <Entry FontAttributes="Italic">
            <Entry.Triggers>
                <Trigger TargetType="Entry" Property="IsFocused" Value="True">
                    <Setter Property="FontAttributes" Value="Bold" />
                </Trigger>
            </Entry.Triggers>
        </Entry>
این کد می‌گوید زمانیکه IsFocused برابر با True است، FontAttributes برابر با Bold شود. دقت کنید که مقدار اولیه FontAttributes در این مثال برابر با Italic بوده‌است. اگر Trigger ای بنا بر شرایطی، مقداری را عوض کند، وقتی آن شرایط برقرار نباشد، کارهایش را ملغی (Rollback) می‌کند. برای همین، در این مثال، وقتی Focus را از روی Entry برداریم، دوباره متن Italic می‌شود!
علاوه بر Trigger، ما DataTrigger نیز داریم. مثال اولیه XamApp که یک Button را داشتیم و با هر بار زدن بر روی آن مقدار StepsCount که به یک Label وصل بود، یکی یکی افزایش میافت، به خاطر بیاورید. فرض کنید در ابتدا که StepsCount برابر با صفر است، قصد نمایش Label را نداریم. مقدار StepsCount که از جنس int است و در View Model است، قرار است اگر صفر باشد، IsVisible مربوط به Label که bool است، False شود. در این صورت داریم:
        <Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}">
            <Label.Triggers>
                <DataTrigger TargetType="Label" Binding="{Binding StepsCount}" Value="0">
                    <Setter Property="IsVisible" Value="False" />
                </DataTrigger>
            </Label.Triggers>
        </Label>
اما ممکن است شرط حتی از این نیز پیچیده‌تر باشد. برای مثال، اگر مقدار StepsCount، عددی زوج بود، FontAttributes این Label به صورت Bold و اگر نه Italic باشد! یک چنین چیزی با Xaml به تنهایی قابلیت پیاده سازی ندارد و باید از CSharp نیز کمک گرفته شود. یک سمت این رابطه int است و سمت دیگر FontAttributes.
در فولدر Converters در فولدر Views در پروژه XamApp کلاس زیر را اضافه کنید:
  public class StepsCountToFontAttributesConverter : Converter<int, FontAttributes>
    {
        protected override FontAttributes Convert(int value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value % 2 == 0) /* راه تشخیص زوج بودن یک عدد */
                return FontAttributes.Bold;
            return FontAttributes.Italic;
        }
    }
سپس در Label مربوطه، در هنگام Bind کردن FontAttributes به StepsCount داریم:
<Label FontAttributes="{Binding StepsCount, Converter={converters:StepsCountToFontAttributesConverter}}"
 Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}" />
به این صورت می‌توانیم روی جزئیات Binding‌ها تسلط کامل داشته باشیم.
علاوه بر موارد فوق، Event Trigger نیز وجود دارد. برای مثال، زمانیکه روی دکمه‌ای کلیک می‌شود، یک Label با Animation ای Fade out شود. برای این کار نگاهی به پروژه  Xamanimation بیاندازید.