نظرات مطالب
بررسی ویجت Kendo UI File Upload
با سلام ؛ من یک مشکل در هنگام ثبت فایل در دیتابس دارم که موقع ثبت چند فایل پشت سر هم در هنگام ثبت کوئری ایی که اطلاعات از دیتابیس هنگام ثبت میگیرند درست عمل نمیکنند و فقط بار اول اعمال می‌شوند
public ActionResult EditSave(IEnumerable<HttpPostedFileBase> files, int referid)
       {
           // OASEntities db = new OASEntities();
           // The Name of the Upload component is "files"
           if (files != null)
           {
 
 
               int i =(int)db.letterdatas.Where(x => x.referletid == referid).Max(p => p.seq);
               foreach (var file in files)
               {
                   bool failtestsize = false;
                   if (file.ContentLength > 5096000) failtestsize = true;
                   if (failtestsize) return Content(new Exception("خطا : حجم فایل ارسالی بیش از حد مجاز می‌باشد").Message);
                 //  System.Threading.Thread.Sleep(2000);
                   var xi = db.letterdatas.Where(x => x.referletid == referid).Count();
                   if (xi > 4) return Content(new Exception("خطا : تعداد فایلهای ارسالی بیش از حد مجاز می‌باشد").Message);
 
 
                   i++;
                    
                  
                   var fileName = Path.GetFileName(file.FileName);
 
 
                 
                   byte[] filedata = new byte[file.ContentLength];
                   Stream st = file.InputStream;
                   st.Read(filedata, 0, file.ContentLength);
                   var letterdata = new letterdata
                   {
                       data = filedata,
                       extention = Path.GetExtension(file.FileName),
                       filename = fileName,
                       seq = i,
                       referletid = referid,
                       templetguid = null,
                       fileid = Guid.NewGuid(),
                       userid = db.users.Where(x => x.username == User.Identity.Name).Select(x => x.userid).FirstOrDefault()
 
 
                   };
                   db.letterdatas.Add(letterdata);
                   db.SaveChanges();
                  
               }
               
           }
            
           return Content("");
       }
تو اکشن بالا فایل از kendo upload دریافت و ثبت می‌شه و لی موقعی که چند فایل ارسال میشه دو کوئری زیر درست عمل نمی‌کنند 
int i =(int)db.letterdatas.Where(x => x.referletid == referid).Max(p => p.seq);
var xi = db.letterdatas.Where(x => x.referletid == referid).Count();
یعنی سرعت ثبت بالاتر از حدی که کوئری‌ها اطلاعات رو از db برای هر ثبت فایل بگیرند ضمنا kendo upload به صورت زیر تنظیم شده بدون ارسال همزمان batch غیرفعال است
@(Html.Kendo().Upload()
           .Name("files")
           .ShowFileList(true)
           .Events(x => x.Select("onxselect").Upload("onxupload").Error("o  nxerror"))
         //  .Multiple(false)
         .HtmlAttributes(
       new
       {
           accept =
               ".pdf"
       })
           .Messages(x => x.Select("انتخاب فایل‌های نامه"))
           .Messages(x => x.Remove("حذف فایل"))
            .Messages(x => x.Cancel("لغو فایل"))
           .Messages(x => x.Retry("دوباره"))
             .Messages(x => x.UploadSelectedFiles("در حال ارسال فایلها ..."))
             .Messages(x => x.HeaderStatusUploaded("کلیه فایلها ارسال شد"))
            .Messages(x => x.DropFilesHere("فایل‌ها را به اینجا بکشید"))
           .Async(a => a
               .Save("EditSave", "Lettersdata", new { referid = @ViewBag.referid })
              .Remove("EditRemove", "Lettersdata", new { referid = @ViewBag.referid })
               .AutoUpload(true)
                
              // .Batch(true)
           )
                   .Files(files =>
                   {
                       foreach (var f in @ViewBag.files)
                       {
                           files.Add().Name(f.filename);
 
 
                       }
                   }
                       )
                       )

مطالب
ایجاد چارت سازمانی تحت وب #4 - آخر
نما : Layout
در یک نمودار یا چارت سازمانی در حد امکان شاخه‌ها همواره در کنار هم و جمع و جور رسم میشوند. در مثال زیر نود u-Node 1 و u-Node 3 دارای زیر شاخه نبوده ، بنابراین نیازی به فضای زیرین جهت نمایش ندارند. جهت مشاهده این فضا میتوانید خط مشخص شده در کد را فعال نمائید و تفاوت فضای مورد نیاز و ایجاد شده را ببینید.
دو درخت نمودار متفاوت در کنار هم رسم شده اند. هیچ همپوشانی بین درختان رسم شده وجود ندارد ( بنابراین نود Root 2 روی نود u-Node 3  رسم نشده است.


var o = new orgChart();

o.addNode( 0, '', '', 'Root 1');
o.addNode( 1,  0, 'u', 'u-Node 1');
o.addNode( 2,  0, 'u', 'u-Node 2');
o.addNode( 3,  0, 'u', 'u-Node 3');
o.addNode( 4,  0, 'u', 'u-Node 4');
//میتوانید خط زیر را فعال نمائید تا تفاوت فضای ایجاد شده و مورد نیاز را مشاهده نمائید.
 //o.addNode( 9, 3, 'u', 'EXTRA', 0, '', '#CC0000', '#FF0000'); o.addNode(11, 2, 'l', 'l-Node'); o.addNode(12, 2, 'u', 'u-Node'); o.addNode(13, 2, 'u', 'u-Node'); o.addNode(14, 2, 'r', 'r-Node'); o.addNode(20, 4, 'l', 'l-Node'); o.addNode(21, '', '', 'Root 2'); o.drawChart('c_layout');
یک مثال کامل :
نمونه زیر یک مثال کامل میباشد. انواع اتصالهای تو در تو چندگانه در این نمونه استفاده شده است .


این هم کد نمونه فوق :

var o = new orgChart();

o.setFont('Arial', 18);
o.addNode( 0, '', '', 'President', 1);

o.setFont('Arial', 12);
o.addNode('',  0, 'l', 'Control');
o.addNode('',  0, 'l', 'Secretary');
o.addNode('',  0, 'l', 'Marketing');
o.addNode('',  0, 'l', 'Human resources');

o.setColor('#99CC99', '#CCFFCC');
o.addNode(12,  0, 'r', 'Facility');
o.addNode('', 12, 'r', 'IT');
o.addNode('', 12, 'r', 'Resource planning');

o.setFont('Arial', 18);
o.setColor('#CCCC66', '#FFFF99');
o.addNode(20,  0, 'u', 'Projects', 1);

o.setFont('Arial', 12);
o.addNode('', 20, 'r', 'Management');
o.addNode('', 20, 'r', 'Finance');
o.addNode('', 20, 'l', 'Development');
o.addNode('', 20, 'l', 'Maintenance');
o.addNode('', 20, 'l', 'Specials');

o.setColor('#CC4950', '#FF7C80');
o.setFont('Arial', 18);
o.addNode(30,  0, 'u', 'Specials', 1);

o.setFont('Arial', 12);
o.addNode(31, 30, 'l', 'Management');
o.addNode('', 30, 'l', 'Communication');
o.addNode(33, 30, 'r', 'Development');
o.addNode(34, 33, 'r', 'Maintenance');
o.addNode('', 33, 'r', 'Special A');
o.addNode('', 33, 'r', 'Special B');
o.addNode('', 33, 'r', 'Advice');
o.addNode('', 30, 'l', 'Taskforce');

o.setColor('#CC9966', '#FFCC99');
o.setFont('Arial', 18);
o.addNode(40,  0, 'u', 'Programming', 1);

o.setFont('Arial', 12);
o.addNode(41, 40, 'l', 'Management');
o.addNode(42, 40, 'l', 'Finance');
o.addNode('', 40, 'r', 'Assessment');
o.addNode('', 40, 'r', 'Coding team');
o.addNode('', 40, 'r', 'Quality control');

o.drawChart('c_ex1', '', true);
اضافه کردن تصویر به نودها :
شما میتوانید به نودها تصویر دلخواه خود را نیز اضافه نمائید. تصاویر بصورت عمودی قرار خواهند گرفت و در صورتی که بزرگ باشند تغییر اندازه خواهند داد. ( فراخوانی تابع setSize قبل از اضافه کردن عکس در این مثال )

کدهای مثال فوق :

var o = new orgChart();

o.setSize(120, 60);

o.setFont('Arial', 18);
o.addNode( 1, '', '', 'Icon smiley', 0, '', '', '', '', 'pic/smiley.gif');
o.addNode( 2, '', '', 'This is a tree', 0, '', '', '', '', 'pic/tree.jpg');
o.addNode( 3, 2, 'u', 'This is a tree');
o.addNode( 4, '', '', 'Right Top smiley', 0, '', '', '', '', 'pic/smiley.gif', 'rt');
o.addNode( 5, '', '', 'Center bottom smiley', 0, '', '', '', '', 'pic/smiley.gif', 'cb');

o.drawChart('c_img');

یک مثال دیگر از استفاده تصاویر در چارت :


var o = new orgChart();

o.setSize(60, 110);

o.setFont('Arial', 12);
o.addNode( 1, '', '', 'Hominidae');
o.addNode( 10, 1, 'l', 'Hominidae');
o.addNode( 11, 10, 'l', 'Hominini');
o.addNode( 12, 10, 'r', 'Gorillini');
o.addNode( 20, 1, 'r', 'Ponginae');
o.addNode( '', 11, '', 'Homo Sapiens', '', '', '', '', '', 'pic/homo.jpg', 'ct');
o.addNode( '', 11, '', 'Pan', '', '', '', '', '', 'pic/pan.jpg', 'ct');
o.addNode( '', 12, '', 'Gorilla', '', '', '', '', '', 'pic/gorilla.jpg', 'ct');
o.addNode( '', 20, '', 'Pongo', '', '', '', '', '', 'pic/pongo.jpg', 'ct');

o.drawChart('c_img2', 'c');
تبدیل تصویر از فرمت jpg به تصویر با فرمت png :
عدم امکان استفاده از مرورگر IE تا نسخه 8 ، چرا که IE  هیچ پشتیبانی از toDataURL در excanvas.js  را انجام نمیدهد.
شما میتوانید از توابع استاندارد canvas در جهت تبدیل محتویات canvas به تصویر استاتیک استفاده نمائید. برای اینکه بتوانید این کار را تست نمائید باید کد ذیل را در همان صفحه ای که کد ( یک مثال کامل ) را استفاده کردید درج نمائید و در این صورت با کلیک بر روی لینک اول میتوانید یک تصویر با فرمت png در یک صفحه جدید از نمودار خود بدست بیاورید و یا با کلیک بر روی لینک دوم یک تصویر را دانلود نمائید.
<script type="text/javascript">
function openAsPng(id){
        window.open(document.getElementById(id).toDataURL("image/png"));
}
function saveAsPng(id){
var img = document.getElementById(id).toDataURL("image/png");
        document.location.href = img.replace("image/png", "image/octet-stream");
}
</script>

<a href = "javascript:openAsPng('c_ex1');">Click here to open the image as png in a new window</a><BR>
<a href = "javascript:saveAsPng('c_ex1');">Click here to save the image as png</a><BR>

نمایش چارت فقط بصورت یک تصویر :

برای اینکه بتوانید یک چارت ایجاد شده از این روش را فقط بصورت یک تصویر نمایش دهید باید عمل تبدیل به عکس را بلافاصله پس از رسم نمودار در canvas انجام دهید بدین صورت که در کد ذیل مشاهده مینمائید:
<canvas id="c_pngchart" width="1" height="1">Your browser does not support canvas!</canvas>

<img id="pngchart">
<script type="text/javascript">
var o = new orgChart();
o.addNode(0, '', '', 'Root');
o.addNode(1, 0, 'u', 'u-Node 1');
o.addNode(2, 0, 'u', 'u-Node 2');
o.drawChart('c_pngchart', '', true);
var canvas = document.getElementById('c_pngchart');
document.getElementById("pngchart").src = canvas.toDataURL("image/png");
// The html keyword "hidden" doesn't work in IE, so resize the canvas to NUL
canvas.height = 0;
canvas.width = 0;
</script>

همه لینکهایی که در نودها ایجاد شده است غیرفعال شده و از کار می‌افتند. بنابراین برای انجام این کار ،یک المنت تصویر در صفحه خود ایجاد ، نمودار را در canvas رسم نموده ، نمودار را به تصویر تبدیل نموده و آن را به المنت تصویر مقید میکنیم و در آخر canvas مخفی میکنیم. برای این منظور از کلید واژه hidden  استفاده میکنیم که در IE این کلمه باز قابل شناسایی نبوده و باید از روش تخصیص اندازه طول و عرض صفر 0 استفاده شود یعنی width=0 , height=0

تصویر فوق ایجاد شده کد مورد نظر میباشد.

تغییر اندازه پویا :
اگر بخواهید بصورت پویا اندازه canvas را تغییر دهید ، نمودار شما ناپدید میشود و پس از تغییر اندازه ، نمودار پاک خواهد شد.
برای رسم نمودار باید دوباره از توابع drawChart() یا redrawChart()   استفاده نمائید.
برای رسم نودها نیازی به تعریف دوباره آنها نمیباشد ( مخصوصا در مثالی که در این صفحه برای شما ارائه شده است )
تابع ()drowChart تمامی نودها را در زمان رسم دوباره جاگذاری میکند ، در صورتی که اگر شما میدانید چارت شما به غیر از اندازه هیچ تغییر دیگری نداشته میتوانید با فراخوانی تابع redrawChart  یک کپی از همان چارت را که در حافظه canvas وجود دارد را رسم نمائید.
از تمامی دوستان خوبم تشکر میکنم که این مطلب را دنبال نمودند . ما را از نظرات خوب و سازنده خود بی نصیب نفرمائید.

نظرات مطالب
بررسی ویجت Kendo UI File Upload
سلام و با تشکر؛ یک سوال:
آیا امکان دسترسی به لیست فایل‌ها در حالت عدم استفاده از batch وجود دارد؟ به عنوان مثال حالتی را در نظر بگیرید که کاربر قرار است یک فرم را تکمیل کرده در این فرم یکی از فیلدها تصاویر می‌باشد (تصاویر درون جدول دیگری ذخیره خواهند شد) یعنی اکشن متد به اینصورت باشد:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(MyModel model, IEnumerable<HttpPostedFileBase> files)
{
            // code
            return View();
}
در اینحالت مقدار دریافتی files نال خواهد بود.
به عنوان راه‌حل جایگزین از روش "فعال سازی ارسال batch " استفاده کردم یعنی به این صورت:
[HttpPost]
        public ActionResult Save(MyModel model, IEnumerable<HttpPostedFileBase> files)
        {
            
                var sModel = new MyViewModel
                {
                    MainPhoto = model.MainPhoto,
                    Phone = model.Phone,
                    Title = model.Title,
                    WebSiteUrl = model.WebSiteUrl,
    // ....
                };
                if (files != null)
                {
                    var images = files.Select(file => new Image
                    {
                        FileName = Helpers.Helper.UploadFile(file)
                    }).ToList();
                    sModel.Images = images;
                }
                _locationService.AddLocation(sModel);
                _uow.SaveAllChanges();
            
            return RedirectToAction("List");
        }

   حالت فوق به خوبی کار میکند اما مشکل آن عدم نمایش قسمت لیست فایل‌ها و همچنین قسمت درصد پیشرفت فایل است. 
بازخوردهای پروژه‌ها
تغییر عرض هر ستون
سلام 
با تشکر از شما و زحمات شما
در هر ستون در صورتی که اندازه مقدار ورودی بیشتر از Width مقدار دهی شده باشد
رکورد مورد نظر شکسته میشود و مقداری از متن به سطر دوم رکورد نمایش داده میشود
که در مواقعی که مقدار عددی در آن ستون قرار دارد این کار باعث سردرگمی میشود
مثل عکس زیر

ولی در بعضی صفحات دیگر گزارش بر اساس اطلاعات

ستون با سایز‌ها منطبق بوده و تغییری پیدا نمیکند

مثلا برای فیلد تاریخ در قسمت قبل به درستی نمایش داده نمیشود

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


متشکرم از شما

نظرات مطالب
Blazor 5x - قسمت 27 - برنامه‌ی Blazor WASM - کار با سرویس‌های Web API
یک نکته: تاثیر hot reload دات نت 6 بر روی درخواست‌های HTTP سمت کلاینت

ممکن است پس از ارتقاء به دات نت 6، شاهد خطاهای BadHttpRequestException زیادی در سمت Web API شوید (البته فقط در حالت توسعه):
Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Unexpected end of request content.
این خطا عنوان می‌کند که درخواست HTTP رسیده ناقص است و در اینجا بیشتر به معنای abort شدن آن در سمت کلاینت است. عکس العمل برنامه‌ی Blazor به یک چنین خطایی که در سمت سرور رخ داده، ریفرش کردن کل آدرس جاری و راه اندازی مجدد برنامه‌است. اما ... چرا این اتفاق رخ داده بود؟

اگر برنامه‌ی شما اطلاعاتی را در پوشه‌ی wwwroot ثبت کند، برای مثال یک بانک اطلاعاتی SQLite را در آن قرار داده‌اید، hot reload حتی این پوشه را هم تحت کنترل خود دارد و با هر تغییری در آن، حتی نوشته شدن یک رکورد در فایل بانک اطلاعاتی، مجددا برنامه را کامپایل می‌کند. همین کامپایل پشت صحنه، سبب abort شدن درخواست‌های HTTP سمت کلاینت آن و مشاهده‌ی خطاهای BadHttpRequestException سمت سرور می‌شود که عدم اطلاع از دلیل آن می‌تواند این تصور را پیش بیاورد که کدهای برنامه مشکلی دارند، اما خیر.
اگر با CLI کار می‌کنید، دستور dotnet watch run --no-hot-reload کمی وضع را بهتر می‌کند و معادل افزودن تنظیم زیر به فایل Properties\launchSettings.json است:
"profiles": {
  "Name.Server": {
      "hotReloadEnabled": false,
راه حل دیگر آن، خارج کردن پوشه‌ی wwwroot از حالت watch است که به صورت زیر در فایل csproj قابل انجام است (در سمت برنامه‌ی web api):
<ItemGroup>
  <Watch Remove="wwwroot\**\*" />
</ItemGroup>
مطالب
دریافت و نمایش فایل‌های PDF در برنامه‌های Blazor WASM
زمانیکه قرار است با فایل‌های باینری واقع در سمت سرور کار کنیم، اگر اکشن متدهای ارائه دهنده‌ی آن‌ها محافظت شده نباشند، برای نمایش و یا دریافت آن‌ها تنها کافی است از آدرس مستقیم این منابع استفاده کرد و در این حالت نیازی به رعایت هیچ نکته‌ی خاصی نیست. اما اگر اکشن متدی در سمت سرور توسط فیلتر Authorize محافظت شده باشد و روش محافظت نیز مبتنی بر کوکی‌ها نباشد، یعنی این کوکی‌ها در طی درخواست‌های مختلف، به صورت خودکار توسط مرورگر به سمت سرور ارسال نشوند، آنگاه نیاز است با استفاده از HttpClient برنامه‌های Blazor WASM، درخواست دسترسی به منبعی را به همراه برای مثال JSON Web Tokens کاربر به سمت سرور ارسال کرد و سپس فایل باینری نهایی را به صورت آرایه‌ای از بایت‌ها دریافت نمود. در این حالت با توجه به ماهیت Ajax ای این این عملیات، برای نمایش و یا دریافت این فایل‌های محافظت شده در مرورگر، نیاز به دانستن نکات ویژه‌ای است که در این مطلب به آن‌ها خواهیم پرداخت.



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

در اینجا کدهای سمت سرور برنامه، نکته‌ی خاصی را به همراه نداشته و صرفا یک فایل PDF ساده (محتوای باینری) را بازگشت می‌دهد:
using Microsoft.AspNetCore.Mvc;

namespace BlazorWasmShowBinaryFiles.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ReportsController : ControllerBase
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            //TODO: create the `sample.pdf` report file on the server

            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
که در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.

یک نکته: استفاده مستقیم از کتابخانه‌های تولید PDF در برنامه‌های سمت کاربر Blazor منطقی نیست؛ چون به ازای هر کاربر، گاهی از اوقات مجبور به ارسال بیش از 8 مگابایت اضافی مختص به فایل‌های dll. آن کتابخانه‌ی تولید PDF خواهیم شد. بنابراین بهتر است تولید PDF را در سمت سرور و در اکشن متدهای Web API انجام داد و سپس فایل نهایی تولیدی را در برنامه‌ی سمت کلاینت، دریافت و یا نمایش داد. به همین جهت در این مثال خروجی نهایی یک چنین عملیات فرضی را توسط یک اکشن متد Web API ارائه داده‌ایم که در بسیاری از موارد حتی می‌تواند توسط فیلتر Authorize نیز محافظت شده باشد.


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در برنامه‌های Blazor نیاز است اینکار را توسط JS Interop آن انجام داد؛ از این جهت که API تولید یک Blob URL، صرفا توسط کدهای جاوا اسکریپتی قابل دسترسی است. به همین جهت فایل جدید Client\wwwroot\site.js را با محتوای زیر ایجاد کرده و همچنین مدخل آن‌را در به انتهای فایل Client\wwwroot\index.html، پیش از بسته شدن تگ body، اضافه می‌کنیم:
window.JsBinaryFilesUtils = {
  createBlobUrl: function (byteArray, contentType) {
    // The byte array in .NET is encoded to base64 string when it passes to JavaScript.
    const numArray = atob(byteArray)
      .split("")
      .map((c) => c.charCodeAt(0));
    const uint8Array = new Uint8Array(numArray);
    const blob = new Blob([uint8Array], { type: contentType });
    return URL.createObjectURL(blob);
  },
  downloadFromUrl: function (fileName, url) {
    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = url;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
    document.body.removeChild(anchor);
  },
  downloadBlazorByteArray: function (fileName, byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.downloadFromUrl(fileName, blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  printFromUrl: function (url) {
    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = url;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  },
  printBlazorByteArray: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.printFromUrl(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
  showUrlInNewTab: function (url) {
    window.open(url);
  },
  showBlazorByteArrayInNewTab: function (byteArray, contentType) {
    const blobUrl = this.createBlobUrl(byteArray, contentType);
    this.showUrlInNewTab(blobUrl);
    URL.revokeObjectURL(blobUrl);
  },
};
توضیحات:
- زمانیکه در برنامه‌های Blazor با استفاده از متد ()HttpClient.GetByteArrayAsync آرایه‌ای از بایت‌های یک فایل باینری را دریافت می‌کنیم، ارسال آن به کدهای جاوااسکریپتی به صورت یک رشته‌ی base64 شده صورت می‌گیرد (JS Interop اینکار را به صورت خودکار انجام می‌دهد). به همین جهت در متد createBlobUrl روش تبدیل این رشته‌ی base64 دریافتی را به آرایه‌ای از بایت‌ها، سپس به یک Blob و در آخر به یک Blob URL، مشاهده می‌کنید.  این Blob Url اکنون آدرس موقتی دسترسی به آرایه‌ای از بایت‌های دریافتی توسط مرورگر است. به همین جهت می‌توان از آن به عنوان src بسیاری از اشیاء HTML استفاده کرد.
- متد downloadFromUrl، کار دریافت یک Url و سپس دانلود خودکار آن‌را انجام می‌دهد. اگر به یک anchor استاندارد HTML، ویژگی download را نیز اضافه کنیم، با کلیک بر روی آن، بجای گشوده شدن این Url، مرورگر آن‌را دریافت خواهد کرد. متد downloadFromUrl کار ساخت لینک و تنظیم ویژگی‌های آن و سپس کلیک بر روی آن‌را به صورت خودکار انجام می‌دهد. از متد downloadFromUrl زمانی استفاده کنید که منبع مدنظر، محافظت شده نباشد و Url آن به سادگی در مرورگر قابل گشودن باشد.
- متد downloadBlazorByteArray همان کار متد downloadFromUrl را انجام می‌دهد؛ با این تفاوت که Url مورد نیاز توسط متد downloadFromUrl را از طریق یک Blob Url تامین می‌کند.
- متد printFromUrl که جهت دسترسی به منابع محافظت نشده طراحی شده‌است، Url یک منبع را دریافت کرده، آن‌را به یک iframe اضافه می‌کند و سپس متد print را بر روی این iframe به صورت خودکار فراخوانی خواهد کرد تا سبب ظاهر شدن صفحه‌ی پیش‌نمایش چاپ شود.
- printBlazorByteArray همان کار متد printFromUrl را انجام می‌دهد؛  با این تفاوت که Url مورد نیاز توسط متد printFromUrl را از طریق یک Blob Url تامین می‌کند.


تهیه‌ی متدهایی الحاقی جهت کار ساده‌تر با JsBinaryFilesUtils

پس از تهیه‌ی JsBinaryFilesUtils فوق، می‌توان با استفاده از کلاس زیر که به همراه متدهایی الحاقی جهت دسترسی به امکانات آن است، کار با متدهای دریافت، نمایش و چاپ فایل‌های باینری را ساده‌تر کرد و از تکرار کدها جلوگیری نمود:
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace BlazorWasmShowBinaryFiles.Client.Utils
{
    public static class JsBinaryFilesUtils
    {
        public static ValueTask<string> CreateBlobUrlAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeAsync<string>("JsBinaryFilesUtils.createBlobUrl", byteArray, contentType);
        }

        public static ValueTask DownloadFromUrlAsync(this IJSRuntime JSRuntime, string fileName, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadFromUrl", fileName, url);
        }

        public static ValueTask DownloadBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            string fileName, byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.downloadBlazorByteArray",
                    fileName, byteArray, contentType);
        }

        public static ValueTask PrintFromUrlAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printFromUrl", url);
        }

        public static ValueTask PrintBlazorByteArrayAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.printBlazorByteArray", byteArray, contentType);
        }

        public static ValueTask ShowUrlInNewTabAsync(this IJSRuntime JSRuntime, string url)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showUrlInNewTab", url);
        }

        public static ValueTask ShowBlazorByteArrayInNewTabAsync(
            this IJSRuntime JSRuntime,
            byte[] byteArray, string contentType)
        {
            return JSRuntime.InvokeVoidAsync("JsBinaryFilesUtils.showBlazorByteArrayInNewTab", byteArray, contentType);
        }
    }
}


اصلاح Content Security Policy سمت سرور جهت ارائه‌ی محتوای blob

پس از دریافت فایل PDF به صورت یک blob، با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. در این حالت در Content Security Policy سمت سرور، نیاز است امکان دسترسی به تصاویر و همچنین اشیاء از نوع blob را نیز آزاد معرفی کنید:
img-src 'self' data: blob:
default-src 'self' blob:
object-src 'self' blob:
در غیراینصورت مرورگر، نمایش یک چنین تصاویر و یا اشیایی را سد خواهد کرد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و نمایش آن در صفحه‌ی جاری

در ادامه کدهای کامل مرتبط با تصویری را که در ابتدای بحث مشاهده کردید، ملاحظه می‌کنید:
@page "/"

@using BlazorWasmShowBinaryFiles.Client.Utils

@inject IJSRuntime JSRuntime
@inject HttpClient HttpClient

<h1>Display PDF Files</h1>
<button class="btn btn-info" @onclick="handlePrintPdf">Print PDF</button>
<button class="btn btn-primary ml-2" @onclick="handleShowPdf">Show PDF</button>
<button class="btn btn-success ml-2" @onclick="handleDownloadPdf">Download PDF</button>

@if(!string.IsNullOrWhiteSpace(PdfBlobUrl))
{
    <section class="card mb-5 mt-3">
        <div class="card-header">
            <h4>using iframe</h4>
        </div>
        <div class="card-body">
            <iframe title="PDF Report" width="100%" height="600" src="@PdfBlobUrl" type="@PdfContentType"></iframe>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using object</h4>
        </div>
        <div class="card-body">
            <object data="@PdfBlobUrl" aria-label="PDF Report" type="@PdfContentType" width="100%" height="100%"></object>
        </div>
    </section>

    <section class="card mb-5">
        <div class="card-header">
            <h4>using embed</h4>
        </div>
        <div class="card-body">
            <embed aria-label="PDF Report" src="@PdfBlobUrl" type="@PdfContentType" width="100%" height="100%">
        </div>
    </section>
}

@code
{
    private const string ReportUrl = "/api/Reports/GetPdfReport";
    private const string PdfContentType = "application/pdf";

    private string PdfBlobUrl;

    private async Task handlePrintPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `PrintFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.PrintBlazorByteArrayAsync(byteArray, PdfContentType);
    }

    private async Task handleDownloadPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `DownloadFromUrlAsync` method.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        await JSRuntime.DownloadBlazorByteArrayAsync("report.pdf", byteArray, PdfContentType);
    }

    private async Task handleShowPdf()
    {
        // Note: Using the `HttpClient` is useful for accessing the protected API's by JWT's (non cookie-based authorization).
        // Otherwise just use the `ReportUrl` as the `src` of the `iframe` directly.
        var byteArray = await HttpClient.GetByteArrayAsync(ReportUrl);
        PdfBlobUrl = await JSRuntime.CreateBlobUrlAsync(byteArray, PdfContentType);
    }

    // Tips:
    // 1- How do I enable/disable the built-in pdf viewer of FireFox
    // https://support.mozilla.org/en-US/kb/disable-built-pdf-viewer-and-use-another-viewer
    // 2- How to configure browsers to use the Adobe PDF plug-in to open PDF files
    // https://helpx.adobe.com/acrobat/kb/pdf-browser-plugin-configuration.html
    // https://helpx.adobe.com/acrobat/using/display-pdf-in-browser.html
    // 3- Microsoft Edge is gaining new PDF reader features within the Windows 10 Fall Creator’s Update (version 1709).
}
توضیحات:
- پس از تهیه‌ی JsBinaryFilesUtils و متدهای الحاقی متناظر با آن، اکنون تنها کافی است با  استفاده از متد ()HttpClient.GetByteArrayAsync، فایل PDF ارائه شده‌ی توسط یک اکشن متد را به صورت آرایه‌ای از بایت‌ها دریافت و سپس به متدهای چاپ (PrintBlazorByteArrayAsync) و دریافت (DownloadBlazorByteArrayAsync) آن ارسال کنیم.
- در مورد نمایش آرایه‌ای از بایت‌های دریافتی، وضعیت کمی متفاوت است. ابتدا باید توسط متد CreateBlobUrlAsync، آدرس موقتی این آرایه را در مرورگر تولید کرد و سپس این آدرس را برای مثال به src یک iframe انتساب دهیم تا PDF را با استفاده از امکانات توکار مرورگر، نمایش دهد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmShowBinaryFiles.zip
نظرات مطالب
React 16x - قسمت 16 - مسیریابی - بخش 2 - پارامترهای مسیریابی
به روزرسانی : در نسخه 6 react-router-dom  موارد زیادی دست خوش تغییر شده اند که می‌توان به زیر اشاره کرد:
از این پس به جای Switch از Routes استفاده کنید و استفاده از این تگ اجباری بوده و در صورت اینکه تگهای Route داخل تگهای Routes قرار داده نشوند خطای آن در کنسول مشاهده میگردد.
<Routes>
<Route path="/product/list" element={<ProductList/>}  />
<Route path="/product/new" element={<NewProduct/>} />
</Routes>
همچنین با جای خصوصیت component خصوصیت element را مورد استفاده قرار دهید و تگ کامپوننت را داخل آن بگذارید. در این حالت مسیریابی نیاز به رعایت ترتیب خاصی نیست و دقیقا باید همان مسیر path وارد شود تا کامپوننت در دسترس قرار گیرد.
برای مسیریابی‌های تو در تو یا Nesting که نسبت به قبل خیلی مفاهیم ساده‌تری دارد. روش پیاده سازی مسیریابی تو در تو به صورت تگ‌های تو در تو آمده و از لحاظ طراحی بسیار به مفهوم آن نزدیک‌تر میباشد و اینکه سعی شده است تمام مدخل‌های Route در یک جا قرار بگیرند:
<Routes>

<Route path="/post" element={<Posts/>}>
  <Route path="images" element={<Images />} />
    <Route path="text" element={<Text />} />
    <Route path="/post" element={<Text />} />
</Route>
</Routes>
و برای لینک دادن :
            <div className="list-group">
  <Link to="/post/images" className="list-group-item list-group-item-action active" aria-current="true">
    Images Post
  </Link>
  <Link to="/post/text" className="list-group-item list-group-item-action">Text Post</Link>
</div>
<Outlet />

در این حالت کاربر به مسیر post هدایت شده و با کلیک بر روی گزینه‌های images و text میتواند پست متنی و تصویری را جدا در زیر گزینه‌ها به جای تگ outlet مشاهده نماید
البته مسیرهای زیرین را نیز می‌توان به شکل زیر هم نوشت:
            <div className="list-group">
  <Link to="images" className="list-group-item list-group-item-action active" aria-current="true">
    Images Post
  </Link>
  <Link to="text" className="list-group-item list-group-item-action">Text Post</Link>
</div>
<Outlet />
در این حالت نیاز است تنها مسیر آخر که قرار است به انتهای آدرس اضافه شود نوشته شود. در صورتی که قصد داشته باشیم بخش پست‌های متنی به طور پیش فرض با آدرس post هم باز شود و الزامی حتما به آدرس post/text نباشد میتوان اینگونه تغییر داد:
<Routes>
<Route path="/post" element={<Posts/>}>
  <Route path="images" element={<Images />} />
    <Route path="text" element={<Text />} />
    <Route path="/post" element={<Text />} />
</Route>
</Routes>
در قسمت مسیریابی همان نام post/ نوشته می‌شود.
در انتها برای صفحاتی مانند NotFound میتوان از علامت * و اشاره به آن کامپوننت استفاده کرد:
<Routes>
<Route path="/product/list" element={<ProductList/>}  />
<Route path="/product/new" element={<NewProduct/>} />
<Route path="/post" element={<Posts/>}>
  <Route path="images" element={<Images />} />
    <Route path="text" element={<Text />} />
    <Route path="/post" element={<Text />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>

نظرات مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC
با سلام
ما مطابق آموزشی که در این مقاله داده شده  از یک اکشن متد برای ذخیره عکس ارسالی تو یک پوشه و سپس برگشت دادن مسیر عکس و از یک اکشن متد دیگه برای ذخیره اطلاعاتی که قراره همراه با فرم ارسال بشن (به همراه مسیر عکس برگشت داده شده)، استفاده میکنیم
مشکلی که ما موقع استفاده از این افزونه باهاش برخوردیم اینه که گاهی اوقات و همونطور که انتظار میره اکشن متد (AddAvatars) که وظیفه ذخیره عکس رو داره اول اجرا میشه و اکشن متد (Add) که وظیفه ذخیره اطلاعات رو داره دوم، ولی گاهی اوقات این ترتیب به هم میریزه و ابتدا اطلاعات ارسالی ذخیره میشه و بعد اکشن متد ذخیره عکس اجرا میشه.
سناریوی ما هم تا حدی شبیه به سناریویی هست که آقای احمدی مطرح کردند، ولی همونطور که گفتیم مشکل اصلی اینه که اکشن متدها هر بار با ترتیب‌های متفاوت فراخوانی میشن
<div class="container-fluid">
    @using (Ajax.BeginForm("Add", "Authors", new AjaxOptions { UpdateTargetId = "result", InsertionMode = InsertionMode.Replace, HttpMethod = "POST" }, new { @class = "form-horizontal", id = "UploadFile" }))
    {
        @Html.AntiForgeryToken()
                
        <div class="control-group">
            <label class="control-label" for="AuhtorFirstNameAndLastName">نام نویسنده</label>
            <div class="controls">
                @Html.TextBoxFor(author => author.AuhtorFirstNameAndLastName, new { placeholder = "نام نویسنده" })
            </div>
            @Html.ValidationMessageFor(author => author.AuhtorFirstNameAndLastName)
        </div>
      
        <div class="control-group">
            <label class="control-label" for="Status">ارسال عکس</label>
            <div class="controls">
                <input type="file" name="avatarFile" id="avatarFile" />
            </div>
            <div>
                @*<input type="submit" name="btn-submit" value="ارسال" class="btn btn-success" />*@
                <img id="loading" alt="1" src="Images/loading83.gif" style="display: none;" />
            </div>
        </div>
       
        <div id="result"></div>
        <input type="submit" name="btn-submit" value="افزودن نویسنده" class="btn btn-success" />
        <input type="button" name="btn-colose" id="btn-close" value="انصراف" class="btn btn-danger" onclick="$dialog.dialog('close');" />
    }
</div>

<script type="text/javascript">
        $('#UploadFile').submit(function () {
            $("#loading").show();
            $.ajaxFileUpload({
                url: "@Url.Action("AddAvatar","Authors")",
                 secureuri: false,
                 fileElementId: 'avatarFile',
                 dataType: 'json',
                 data: {}, 
                 success: function (data, status) {
                     $("#loading").hide();
                 },
                 error: function (data, status, e) {
                     $("#loading").hide();
                 }
             });
         });
</script>
مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت دوم - سرویس اعتبارسنجی
در قسمت قبل، ساختار ابتدایی کلاینت Angular را تدارک دیدیم. در این قسمت قصد داریم سرویسی که زیر ساخت کامپوننت لاگین و عملیات ورود به سیستم را تامین می‌کند، تکمیل کنیم.


تعریف تزریق وابستگی تنظیمات برنامه

در مطلب «تزریق وابستگی‌ها فراتر از کلاس‌ها در برنامه‌های Angular» با روش تزریق ثوابت برنامه آشنا شدیم. در این مثال، برنامه‌ی کلاینت بر روی پورت 4200 اجرا می‌شود و برنامه‌ی سمت سرور وب، بر روی پورت 5000. به همین جهت نیاز است این آدرس پایه سمت سرور را در تمام قسمت‌های برنامه که با سرور کار می‌کنند، در دسترس داشته باشیم و روش مناسب برای پیاده سازی آن همان قسمت «تزریق تنظیمات برنامه توسط تامین کننده‌ی مقادیر» مطلب یاد شده‌است. به همین جهت فایل جدید src\app\core\services\app.config.ts را در پوشه‌ی core\services برنامه ایجاد می‌کنیم:
import { InjectionToken } from "@angular/core";

export let APP_CONFIG = new InjectionToken<string>("app.config");

export interface IAppConfig {
  apiEndpoint: string;
  loginPath: string;
  logoutPath: string;
  refreshTokenPath: string;
  accessTokenObjectKey: string;
  refreshTokenObjectKey: string;
}

export const AppConfig: IAppConfig = {
  apiEndpoint: "http://localhost:5000/api",
  loginPath: "account/login",
  logoutPath: "account/logout",
  refreshTokenPath: "account/RefreshToken",
  accessTokenObjectKey: "access_token",
  refreshTokenObjectKey: "refresh_token"
};
در اینجا APP_CONFIG یک توکن منحصربفرد است که از آن جهت یافتن مقدار AppConfig که از نوع اینترفیس IAppConfig تعریف شده‌است، در سراسر برنامه استفاده خواهیم کرد.
سپس تنظیمات ابتدایی تزریق وابستگی‌های IAppConfig را در فایل src\app\core\core.module.ts به صورت ذیل انجام می‌دهیم:
import { AppConfig, APP_CONFIG } from "./app.config";

@NgModule({
  providers: [
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}
اکنون هر سرویس و یا کامپوننتی در سراسر برنامه که نیاز به تنظیمات AppConfig را داشته باشد، کافی است با استفاده از ویژگی Inject(APP_CONFIG)@ آن‌را درخواست کند.


طراحی سرویس Auth

پس از لاگین باید بتوان به اطلاعات اطلاعات کاربر وارد شده‌ی به سیستم، در تمام قسمت‌های برنامه دسترسی پیدا کرد. به همین جهت نیاز است این اطلاعات را در یک سرویس سراسری singleton قرار داد تا همواره یک وهله‌ی از آن در کل برنامه قابل استفاده باشد. مرسوم است این سرویس را AuthService بنامند. بنابراین محل قرارگیری این سرویس سراسری در پوشه‌ی Core\services و محل تعریف آن در قسمت providers آن خواهد بود. به همین جهت ابتدا ساختار این سرویس را با دستور ذیل ایجاد می‌کنیم:
 ng g s Core/services/Auth
با این خروجی:
   create src/app/Core/services/auth.service.ts (110 bytes)
و سپس تعریف آن‌را به مدخل providers ماژول Core اضافه می‌کنیم:
import { AuthService } from "./services/auth.service";

@NgModule({
  providers: [
    // global singleton services of the whole app will be listed here.
    BrowserStorageService,
    AuthService,
    { provide: APP_CONFIG, useValue: AppConfig }
  ]
})
export class CoreModule {}

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


اطلاع رسانی به کامپوننت Header در مورد وضعیت لاگین

در مطلب «صدور رخدادها از سرویس‌ها به کامپوننت‌ها در برنامه‌های Angular» با نحوه‌ی کار با BehaviorSubject آشنا شدیم. در اینجا می‌خواهیم توسط آن، پس از لاگین موفق، وضعیت لاگین را به کامپوننت هدر صادر کنیم، تا لینک لاگین را مخفی کرده و لینک خروج از سیستم را نمایش دهد:
import { BehaviorSubject } from "rxjs/BehaviorSubject";

@Injectable()
export class AuthService {

  private authStatusSource = new BehaviorSubject<boolean>(false);
  authStatus$ = this.authStatusSource.asObservable();

  constructor() {
    this.updateStatusOnPageRefresh();
  }

  private updateStatusOnPageRefresh(): void {
    this.authStatusSource.next(this.isLoggedIn());
  }
اکنون تمام کامپوننت‌های برنامه می‌توانند مشترک $authStatus شده و همواره آخرین وضعیت لاگین را دریافت کنند و نسبت به تغییرات آن عکس العمل نشان دهند (برای مثال قسمتی را نمایش دهند و یا قسمتی را مخفی کنند).
در اینجا در سازنده‌ی کلاس، بر اساس خروجی متد وضعیت لاگین شخص، برای اولین بار، متد next این BehaviorSubject فراخوانی می‌شود. علت قرار دادن این متد در سازنده‌ی کلاس سرویس، عکس العمل نشان دادن به refresh کامل صفحه، توسط کاربر است و یا عکس العمل نشان دادن به وضعیت به‌خاطر سپاری کلمه‌ی عبور، در اولین بار مشاهده‌ی سایت و برنامه. در این حالت متد isLoggedIn، کش مرورگر را بررسی کرده و با واکشی توکن‌ها و اعتبارسنجی آن‌ها، گزارش وضعیت لاگین را ارائه می‌دهد. پس از آن، خروجی آن (true/false) به مشترکین اطلاع رسانی می‌شود.
در ادامه، متد next این  BehaviorSubject را در متدهای login و logout نیز فراخوانی خواهیم کرد.


تدارک ذخیره سازی توکن‌ها در کش مرورگر

از طرف سرور، دو نوع توکن access_token و refresh_token را دریافت می‌کنیم. به همین جهت یک enum را جهت مشخص سازی آن‌ها تعریف خواهیم کرد:
export enum AuthTokenType {
   AccessToken,
   RefreshToken
}
سپس باید این توکن‌ها را پس از لاگین موفق در کش مرورگر ذخیره کنیم که با مقدمات آن در مطلب «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular» پیشتر آشنا شدیم. از همان سرویس BrowserStorageService مطلب یاد شده، در اینجا نیز استفاده خواهیم کرد:
import { BrowserStorageService } from "./browser-storage.service";

export enum AuthTokenType {
  AccessToken,
  RefreshToken
}

@Injectable()
export class AuthService {

  private rememberMeToken = "rememberMe_token";

  constructor(private browserStorageService: BrowserStorageService) {  }

  rememberMe(): boolean {
    return this.browserStorageService.getLocal(this.rememberMeToken) === true;
  }

  getRawAuthToken(tokenType: AuthTokenType): string {
    if (this.rememberMe()) {
      return this.browserStorageService.getLocal(AuthTokenType[tokenType]);
    } else {
      return this.browserStorageService.getSession(AuthTokenType[tokenType]);
    }
  }

  deleteAuthTokens() {
    if (this.rememberMe()) {
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeLocal(AuthTokenType[AuthTokenType.RefreshToken]);
    } else {
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.AccessToken]);
      this.browserStorageService.removeSession(AuthTokenType[AuthTokenType.RefreshToken]);
    }
    this.browserStorageService.removeLocal(this.rememberMeToken);
  }

  private setLoginSession(response: any): void {
    this.setToken(AuthTokenType.AccessToken, response[this.appConfig.accessTokenObjectKey]);
    this.setToken(AuthTokenType.RefreshToken, response[this.appConfig.refreshTokenObjectKey]);
  }

  private setToken(tokenType: AuthTokenType, tokenValue: string): void {
    if (this.rememberMe()) {
      this.browserStorageService.setLocal(AuthTokenType[tokenType], tokenValue);
    } else {
      this.browserStorageService.setSession(AuthTokenType[tokenType], tokenValue);
    }
  }
}
ابتدا سرویس BrowserStorageService به سازنده‌ی کلاس تزریق شده‌است و سپس نیاز است بر اساس گزینه‌ی «به‌خاطر سپاری کلمه‌ی عبور»، نسبت به انتخاب محل ذخیره سازی توکن‌ها اقدام کنیم. اگر گزینه‌ی rememberMe توسط کاربر در حین لاگین انتخاب شود، از local storage ماندگار و اگر خیر، از session storage فرار مرورگر برای ذخیره سازی توکن‌ها و سایر اطلاعات مرتبط استفاده خواهیم کرد.


- متد rememberMe مشخص می‌کند که آیا وضعیت به‌خاطر سپاری کلمه‌ی عبور توسط کاربر انتخاب شده‌است یا خیر؟ این مقدار را نیز در local storage ماندگار ذخیره می‌کنیم تا در صورت بستن مرورگر و مراجعه‌ی مجدد به آن، در دسترس باشد و به صورت خودکار پاک نشود.
- متد setToken، بر اساس وضعیت rememberMe، مقادیر توکن‌های دریافتی از سرور را در local storage و یا session storage ذخیره می‌کند.
- متد getRawAuthToken بر اساس یکی از مقادیر enum ارسالی به آن، مقدار خام access_token و یا refresh_token ذخیره شده را بازگشت می‌دهد.
- متد deleteAuthTokens جهت حذف تمام توکن‌های ذخیره شده‌ی توسط برنامه استفاده خواهد شد. نمونه‌ی کاربرد آن در متد logout است.
- متد setLoginSession پس از لاگین موفق فراخوانی می‌شود. کار آن ذخیره سازی توکن‌های دریافتی از سرور است. فرض آن نیز بر این است که خروجی json از طرف سرور، توکن‌ها را با کلیدهایی دقیقا مساوی access_token و refresh_token بازگشت می‌دهد:
 {"access_token":"...","refresh_token":"..."}
اگر این کلیدها در برنامه‌ی شما نام دیگری را دارند، محل تغییر آن‌ها در فایل app.config.ts است.


تکمیل متد ورود به سیستم

در صفحه‌ی لاگین، کاربر نام کاربری، کلمه‌ی عبور و همچنین گزینه‌ی «به‌خاطر سپاری ورود» را باید تکمیل کند. به همین جهت اینترفیسی را برای این کار به نام Credentials در محل src\app\core\models\credentials.ts ایجاد می‌کنیم:
export interface Credentials {
   username: string;
   password: string;
   rememberMe: boolean;
}
پس از آن در متد لاگین از این اطلاعات جهت دریافت توکن‌های دسترسی و به روز رسانی، استفاده خواهیم کرد:
@Injectable()
export class AuthService {
  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private browserStorageService: BrowserStorageService   
  ) {
    this.updateStatusOnPageRefresh();
  }

  login(credentials: Credentials): Observable<boolean> {
    const headers = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .post(`${this.appConfig.apiEndpoint}/${this.appConfig.loginPath}`, credentials, { headers: headers })
      .map((response: any) => {
        this.browserStorageService.setLocal(this.rememberMeToken, credentials.rememberMe);
        if (!response) {
          this.authStatusSource.next(false);
          return false;
        }
        this.setLoginSession(response);
        this.authStatusSource.next(true);
        return true;
      })
      .catch((error: HttpErrorResponse) => Observable.throw(error));
  }
}
متد login یک Observable از نوع boolean را بازگشت می‌دهد. به این ترتیب می‌توان مشترک آن شد و در صورت دریافت true یا اعلام لاگین موفق، کاربر را به صفحه‌ای مشخص هدایت کرد.
در اینجا نیاز است اطلاعات شیء Credentials را به مسیر http://localhost:5000/api/account/login ارسال کنیم. به همین جهت نیاز به سرویس IAppConfig تزریق شده‌ی در سازنده‌ی کلاس وجود دارد تا با دسترسی به this.appConfig.apiEndpoint، مسیر تنظیم شده‌ی در فایل src\app\core\services\app.config.ts را دریافت کنیم.
پس از لاگین موفق:
- ابتدا وضعیت rememberMe انتخاب شده‌ی توسط کاربر را در local storage مرورگر جهت مراجعات آتی ذخیره می‌کنیم.
- سپس متد setLoginSession، توکن‌های دریافتی از شیء response را بر اساس وضعیت rememberMe در local storage ماندگار و یا session storage فرار، ذخیره می‌کند.
- در آخر با فراخوانی متد next مربوط به authStatusSource با پارامتر true، به تمام کامپوننت‌های مشترک به این سرویس اعلام می‌کنیم که وضعیت لاگین موفق بوده‌است و اکنون می‌توانید نسبت به آن عکس العمل نشان دهید.


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

کار خروج، با فراخوانی متد logout صورت می‌گیرد:
@Injectable()
export class AuthService {

  constructor(
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
    private http: HttpClient,
    private router: Router
  ) {
    this.updateStatusOnPageRefresh();
  }

  logout(navigateToHome: boolean): void {
    this.http
      .get(`${this.appConfig.apiEndpoint}/${this.appConfig.logoutPath}`)
      .finally(() => {
        this.deleteAuthTokens();
        this.unscheduleRefreshToken();
        this.authStatusSource.next(false);
        if (navigateToHome) {
          this.router.navigate(["/"]);
        }
      })
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        console.log("logout", result);
      });
  }
}
در اینجا در ابتدا متد logout سمت سرور که در مسیر http://localhost:5000/api/account/logout قرار دارد فراخوانی می‌شود. پس از آن در پایان کار در متد finally (چه عملیات فراخوانی logout سمت سرور موفق باشد یا خیر)، ابتدا توسط متد deleteAuthTokens تمام توکن‌ها و اطلاعات ذخیره شده‌ی در مرورگر حذف می‌شوند. در ادامه با فراخوانی متد next مربوط به authStatusSource با مقدار false، به تمام مشترکین سرویس جاری اعلام می‌کنیم که اکنون وقت عکس العمل نشان دادن به خروجی سیستم و به روز رسانی رابط کاربری است. همچنین اگر پارامتر navigateToHome نیز مقدار دهی شده بود، کاربر را به صفحه‌ی اصلی برنامه هدایت می‌کنیم.


اعتبارسنجی وضعیت لاگین و توکن‌های ذخیره شده‌ی در مرورگر

برای اعتبارسنجی access token دریافتی از طرف سرور، نیاز به بسته‌ی jwt-decode است. به همین جهت دستور ذیل را در خط فرمان صادر کنید تا بسته‌ی آن به پروژه اضافه شود:
 > npm install jwt-decode --save
در ادامه برای استفاده‌ی از آن، ابتدا بسته‌ی آن‌را import می‌کنیم:
 import * as jwt_decode from "jwt-decode";
و سپس توسط متد jwt_decode آن می‌توان به اصل اطلاعات توکن دریافتی از طرف سرور، دسترسی یافت:
  getDecodedAccessToken(): any {
    return jwt_decode(this.getRawAuthToken(AuthTokenType.AccessToken));
  }
این توکن خام، پس از decode، یک چنین فرمت نمونه‌ای را دارد که در آن، شماره‌ی کاربری (nameidentifier)، نام کاربری (name)، نام نمایشی کاربر (DisplayName)، نقش‌های او (قسمت role) و اطلاعات تاریخ انقضای توکن (خاصیت exp)، مشخص هستند:
{
  "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a",
  "iss": "http://localhost/",
  "iat": 1513070340,
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
  "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid",
  "DisplayName": "وحید",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [
    "Admin",
    "User"
  ],
  "nbf": 1513070340,
  "exp": 1513070460,
  "aud": "Any"
}
برای مثال اگر خواستیم به خاصیت DisplayName این شیء decode شده دسترسی پیدا کنیم، می‌توان به صورت ذیل عمل کرد:
  getDisplayName(): string {
    return this.getDecodedAccessToken().DisplayName;
  }
و یا خاصیت exp آن، بیانگر تاریخ انقضای توکن است. برای تبدیل آن به نوع Date، ابتدا باید به این خاصیت در توکن decode شده دسترسی یافت و سپس توسط متد setUTCSeconds آن‌را تبدیل به نوع Date کرد:
  getAccessTokenExpirationDateUtc(): Date {
    const decoded = this.getDecodedAccessToken();
    if (decoded.exp === undefined) {
      return null;
    }
    const date = new Date(0); // The 0 sets the date to the epoch
    date.setUTCSeconds(decoded.exp);
    return date;
  }
اکنون که به این تاریخ انقضای توکن دسترسی یافتیم، می‌توان از آن جهت تعیین اعتبار توکن ذخیره شده‌ی در مرورگر، استفاده کرد:
  isAccessTokenTokenExpired(): boolean {
    const expirationDateUtc = this.getAccessTokenExpirationDateUtc();
    if (!expirationDateUtc) {
      return true;
    }
    return !(expirationDateUtc.valueOf() > new Date().valueOf());
  }
و در آخر متد isLoggedIn که وضعیت لاگین بودن کاربر جاری را مشخص می‌کند، به صورت ذیل تعریف می‌شود:
  isLoggedIn(): boolean {
    const accessToken = this.getRawAuthToken(AuthTokenType.AccessToken);
    const refreshToken = this.getRawAuthToken(AuthTokenType.RefreshToken);
    const hasTokens = !this.isEmptyString(accessToken) && !this.isEmptyString(refreshToken);
    return hasTokens && !this.isAccessTokenTokenExpired();
  }

  private isEmptyString(value: string): boolean {
    return !value || 0 === value.length;
  }
ابتدا بررسی می‌کنیم که آیا توکن‌های درخواست شده‌ی از کش مرورگر، وجود خارجی دارند یا خیر؟ پس از آن تاریخ انقضای access token را نیز بررسی می‌کنیم. تا همین اندازه جهت تعیین اعتبار این توکن‌ها در سمت کاربر کفایت می‌کنند. در سمت سرور نیز این توکن‌ها به صورت خودکار توسط برنامه تعیین اعتبار شده و امضای دیجیتال آن‌ها بررسی می‌شوند.

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


کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.