مطالب
WebStorage: قسمت دوم
در این مقاله قصد داریم نحوه‌ی کدنویسی webstorage را با کتابخانه‌هایی که در مقاله قبل معرفی کردیم بررسی کنیم.
ابتدا روش ذخیره سازی و بازیابی متداول آن را بررسی میکنیم که تنها توسط دو تابع صورت می‌گیرد. مطلب زیر برگرفته از w3Schools است:
دسترسی به شیء webstorage به صورت زیر امکان پذیر است:
window.localStorage
window.sessionStorage
ولی بهتر است قبل از ذخیره و بازیابی، از پشتیبانی مرورگر از webstorage اطمینان حاصل نمایید:
if(typeof(Storage) !== "undefined") {
    // Code for localStorage/sessionStorage.
} else {
    // Sorry! No Web Storage support..
}
برای ذخیره سازی و سپس خواندن به شکل زیر عمل می‌کنیم:
localStorage.setItem("lastname", "Smith");

//========================
localStorage.getItem("lastname");
خواندن می‌تواند حتی به شکل زیر هم صورت بگیرد:
var a=localStorage.lastname;
استفاده از store.js برای مرورگرهایی که از webstorage پشتیبانی نمی‌کنند به شکل زیر است:
//ذخیره مقدار
store.set('username', 'marcus')

//بازیابی مقدار
store.get('username')

//حذف مقدار
store.remove('username')

//حذف تمامی مقادیر ذخیره شده
store.clear()

//ذخیره ساختار
store.set('user', { name: 'marcus', likes: 'javascript' })

//بازیابی ساختار به شکل قبلی
var user = store.get('user')
alert(user.name + ' likes ' + user.likes)

//تغییر مستقیم مقدار قبلی
store.getAll().user.name == 'marcus'

//بازخوانی تمام مقادیر ذخیر شده توسط یک حلقه
store.forEach(function(key, val) {
    console.log(key, '==', val)
})
همچنین بهتر هست از یک فلگ برای بررسی فعال بودن storage استفاده نمایید. به این دلیل که گاهی کاربرها از پنجره‌های private استفاده می‌کنند که ردگیری اطلاعات آن ممکن نیست و موجب خطا می‌شود.
<script src="store.min.js"></script>
<script>
    init()
    function init() {
        if (!store.enabled) {
            alert('Local storage is not supported by your browser. Please disable "Private Mode", or upgrade to a modern browser.')
            return
        }
        var user = store.get('user')
        // ... and so on ...
    }
</script>
در صورتیکه بخشی از داده‌ها را توسط localstorage ذخیره نمایید و بخواهید از طریق storage به آن دسترسی داشته باشید، خروجی string خواهد بود؛ صرف نظر از اینکه شما عدد، شیء یا آرایه‌ای را ذخیره کرده‌اید.
در صورتیکه ساختار JSON را ذخیره کرده باشید، می‌توانید رشته برگردانده شده را با json.stringify و json.parse بازیابی و به روز رسانی کنید.
در حالت cross browser تهیه‌ی یک sessionStorage امکان پذیر نیست. ولی می‌توان به روش ذیل و تعیین یک زمان انقضاء آن را محدود کرد:
var storeWithExpiration = {
// دریافت کلید و مقدار و زمان انقضا به میلی ثانیه
    set: function(key, val, exp) {
//ایجاد زمان فعلی جهت ثبت تاریخ ایجاد
        store.set(key, { val:val, exp:exp, time:new Date().getTime() })
    },
    get: function(key) {
        var info = store.get(key)
//در صورتی که کلید داده شده مقداری نداشته باشد نال را بر میگردانیم
        if (!info) { return null }
//تاریخ فعلی را منهای تاریخ ثبت شده کرده و در صورتی که
//از مقدار میلی ثاینه بیشتر باشد یعنی منقضی شده و نال بر میگرداند
        if (new Date().getTime() - info.time > info.exp) { return null }
        return info.val
    }
}

// استفاده عملی از کد بالا
// استفاده از تایمر جهت نمایش واکشی داده‌ها قبل از نقضا و بعد از انقضا
storeWithExpiration.set('foo', 'bar', 1000)
setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 500) // -> "bar"
setTimeout(function() { console.log(storeWithExpiration.get('foo')) }, 1500) // -> null

مورد بعدی استفاده از سورس  cross-storage  است. اگر به یاد داشته باشید گفتیم یکی از احتمالاتی که برای ما ایجاد مشکل می‌کند، ساب دومین هاست که ممکن است دسترسی ما به یک webstorage را از ساب دومین دیگر از ما بگیرد.
این کتابخانه به دو جز تقسیم شده است یکی هاب Hub و دیگری Client .
ابتدا نیاز است که هاب را آماده سازی و با ارائه یک الگو از آدرس وب، مجوز عملیات را دریافت کنیم. در صورتیکه این مرحله به فراموشی سپرده شود، انجام هر نوع عمل روی دیتاها در نظر گرفته نخواهد شد.
CrossStorageHub.init([
  {origin: /\.example.com$/,            allow: ['get']},
  {origin: /:\/\/(www\.)?example.com$/, allow: ['get', 'set', 'del']}
]);
حرف $ در انتهای عبارت باعث مشود که دامنه‌ها با دقت بیشتری در Regex بررسی شوند و دامنه زیر را معتبر اعلام کند:
valid.example.com
ولی دامنه زیر را نامعتبر اعلام می‌کند:
invalid.example.com.malicious.com
همچنین می‌توانید تنظیماتی را جهت هدر‌های CSP و CORS، نیز اعمال نمایید:
{
  'Access-Control-Allow-Origin':  '*',
  'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE',
  'Access-Control-Allow-Headers': 'X-Requested-With',
  'Content-Security-Policy':      "default-src 'unsafe-inline' *",
  'X-Content-Security-Policy':    "default-src 'unsafe-inline' *",
  'X-WebKit-CSP':                 "default-src 'unsafe-inline' *",
}
پس کار را بدین صورت آغاز می‌کنیم، یک فایل به نام hub.htm درست کنید و هاب را آماده سازید:
hub.htm
<script type="text/javascript" src="~/Scripts/cross-storage/hub.js"></script>
<script>
 CrossStorageHub.init([
      {origin: /.*localhost:300\d$/, allow: ['get', 'set', 'del']}
    ]);
  </script>
کد بالا فقط درخواست‌های هاست لوکال را از پورتی که ابتدای آن با 300 آغاز می‌شود، پاسخ می‌دهد و مابقی درخواست‌ها را رد می‌کند. متدهای ایجاد، ویرایش و حذف برای این آدرس معتبر اعلام شده است. 
در فایل دیگر که کلاینت شناخته می‌شود باید فایل hub معرفی شود تا تنظیمات هاب خوانده شود:
var storage = new CrossStorageClient('http://localhost:3000/example/hub.html');

var setKeys = function () {
      
return storage.set('key1', 'foo').then(function() {
return storage.set('key2', 'bar');
      });
};
در خط اول، فایل هاب معرفی شده و تنظیمات روی این صفحه اعمال می‌شود. سپس در خطوط بعدی داده‌ها ذخیره می‌شوند. از آنجا که با هر یکبار ذخیره، return صورت می‌گیرد و تنها اجازه‌ی ورود یک داده را داریم، برای حل این مشکل متد then پیاده سازی شده است. متغیر setKeys شامل یک آرایه از کلیدها خواهد بود.
نحوه‌ی ذخیره سازی بدین شکل هم طبق مستندات صحیح است:
storage.onConnect().then(function() {

  return storage.set('key', {foo: 'bar'});

}).then(function() {

  return storage.set('expiringKey', 'foobar', 10000);

});
در کد بالا ابتدا یک داده دائم ذخیره شده است و در کد بعد یک داده موقت که بعد از 10 ثانیه اعتبار خود را از دست می‌دهد.
برای خواندن داده‌های ذخیره شده به نحوه زیر عمل می‌کنیم:
storage.onConnect().then(function() {
  return storage.get('key1');
}).then(function(res) {
  return storage.get('key1', 'key2', 'key3');
}).then(function(res) {
  // ...
});
کد بالا نحوه‌ی خواندن مقادیر را به شکل‌های مختلفی نشان میدهد و مقدار بازگشتی آن‌ها یک آرایه از مقادیر است؛ مگر اینکه تنها یک مقدار برگشت داده شود. مقدار بازگشتی در تابع بعدی به عنوان یک آرگومان در دسترس است. در صورتی که خطایی رخ دهد، قابلیت هندل آن نیز وجود دارد:
storage.onConnect()
    .then(function() {
      return storage.get('key1', 'key2');
    })

.then(function(res) {
      console.log(res); // ['foo', 'bar']
    })['catch'](function(err) {
      console.log(err);
    });
برای باقی مسائل چون به دست آوردن لیست کلیدهای ذخیره شده، حذف کلید‌های مشخص شده، پاکسازی کامل داده‌ها و ... به مستندات رجوع کنید.
در اینجا جهت سازگاری با مرورگرهای قدیمی خط زیر را به صفحه اضافه کنید:
<script src="https://s3.amazonaws.com/es6-promises/promise-1.0.0.min.js"></script>
ذخیره‌ی اطلاعات به شکل یونیکد، فضایی دو برابر کدهای اسکی میبرد و با توجه به محدود بودن حجم webstorage به 5 مگابایت ممکن است با کمبود فضا مواجه شوید. در صورتیکه قصد فشرده سازی اطلاعات را دارید می‌توانید از کتابخانه lz-string استفاده کنید. ولی توجه به این نکته ضروری است که در صورت نیاز، عمل فشرده سازی را انجام دهید و همینطوری از آن استفاده نکنید.

IndexedDB API
آخرین موردی که بررسی می‌شود استفاده از IndexedDB API است که با استفاده از آن میتوان با webstorage همانند یک دیتابیس رفتار کرد و به سمت آن کوئری ارسال کرد. 
var request = indexedDB.open("library");

request.onupgradeneeded = function() {
  // The database did not previously exist, so create object stores and indexes.
  var db = request.result;
  var store = db.createObjectStore("books", {keyPath: "isbn"});
  var titleIndex = store.createIndex("by_title", "title", {unique: true});
  var authorIndex = store.createIndex("by_author", "author");

  // Populate with initial data.
  store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
  store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
  store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});
};

request.onsuccess = function() {
  db = request.result;
};
کد بالا ابتدا به دیتابیس library متصل می‌شود و اگر وجود نداشته باشد، آن را می‌سازد. رویداد onupgradeneeded برای اولین بار اجرا شده و در آن می‌توانید به ایجاد جداول و اضافه کردن داده‌های اولیه بپردازید؛ یا اینکه از آن جهت به ارتقاء ورژن دیتابیس استفاده کنید. خصوصیت result، دیتابیس باز شده یا ایجاد شده را باز می‌گرداند. در خط بعدی جدولی با کلید کد ISBN کتاب تعریف شده است. در ادامه هم دو ستون اندیس شده برای عنوان کتاب و نویسنده معرفی شده است که عنوان کتاب را یکتا و بدون تکرار در نظر گرفته است. سپس در جدولی که متغیر store به آن اشاره می‌کند، با استفاده از متد put، رکوردها داخل آن درج می‌شوند. در صورتیکه کار با موفقیت انجام شود رویداد onSuccess فراخوانی می‌گردد.
برای انجام عملیات خواندن و نوشتن باید از تراکنش‌ها استفاده کرد:
var tx = db.transaction("books", "readwrite");
var store = tx.objectStore("books");

store.put({title: "Quarry Memories", author: "Fred", isbn: 123456});
store.put({title: "Water Buffaloes", author: "Fred", isbn: 234567});
store.put({title: "Bedrock Nights", author: "Barney", isbn: 345678});

tx.oncomplete = function() {
  // All requests have succeeded and the transaction has committed.
};
در خط  اول ابتدا یک خط تراکنشی بین ما و جدول books با مجوز خواندن و نوشتن باز می‌شود و در خط بعدی جدول books را در اختیار می‌گیریم و همانند کد قبلی به درج داده‌ها می‌پردازیم. در صورتیکه عملیات با موفقیت به اتمام برسد، متغیر تراکنش رویدادی به نام oncomplete فراخوانی می‌گردد. در صورتیکه قصد دارید تنها مجوز خواندن داشته باشید، عبارت readonly را به کار ببرید.
var tx = db.transaction("books", "readonly");
var store = tx.objectStore("books");
var index = store.index("by_author");

var request = index.openCursor(IDBKeyRange.only("Fred"));
request.onsuccess = function() {
  var cursor = request.result;
  if (cursor) {
    // Called for each matching record.
    report(cursor.value.isbn, cursor.value.title, cursor.value.author);
    cursor.continue();
  } else {
    // No more matching records.
    report(null);
  }
};
در دو خط اول مثل قبل، تراکنش را دریافت می‌کنیم و از آنجا که می‌خواهیم داده را بر اساس نام نویسنده واکشی کنیم، ستون اندیس شده نام نویسنده را دریافت کرده و با استفاده از متد opencursor درخواست خود را مبنی بر واکشی رکوردهایی که نام نویسنده fred است، ارسال میداریم. در صورتیکه عملیات با موفقیت انجام گردد و خطایی دریافت نکنیم رویداد onsuccess فراخوانی می‌گردد. در این رویداد با دو حالت برخورد خواهیم داشت؛ یا داده‌ها یافت می‌شوند و رکوردها برگشت داده می‌شوند یا هیچ رکوردی یافت نشده و مقدار نال برگشت خواهد خورد. با استفاده از cursor.continue می‌توان داده‌ها را به ترتیب واکشی کرده و مقادیر رکورد را با استفاده خصوصیت value  به سمت تابع report ارسال کرد.
کدهای بالا همه در مستندات معرفی شده وجود دارند و ما پیشتر توضیح ابتدایی در مورد آن دادیم و برای کسب اطلاعات بیشتر می‌توانید به همان مستندات معرفی شده رجوع کنید. برای idexedDB هم می‌توانید از این منابع +  +  +  استفاده کنید که خود w3c منبع فوق العاده‌تری است.

نظرات مطالب
(Domain Driven Design) به زبان ساده

عنوان مطلب هست «به زبان ساده»، ولی اصل PI و Specification pattern و مانند اون در مقدمه مطلب بکار رفتن که کمی برای قسمت اول زیاده روی هست. ضمنا Refactoring اصلا به معنای بهبود و توسعه همزمان سیستم نیست. Refactoring بهبود کیفیت کدها هست بدون تغییری در ساختار کلی سیستم.

نظرات مطالب
ASP.NET MVC #12
به زبان ساده: این‌ها رو یک سری syntax درنظر بگیرید.
مطابق میل طراح Razor، زمانیکه model با m کوچک مطرح می‌شود، یعنی قرار است جنس مدل مورد استفاده صریحا ذکر شود.
زمانیکه از Model با M بزرگ استفاده می‌شود، یعنی با یک وهله از مدلی با جنس مشخص شده سر و کار داریم.
نظرات مطالب
آموزش مهندسی نرم افزار و UML - جلسه اول
دقیقا از نام uml مشخصه که فقط یک زبان مشترکه و ربطی به سایر مسائل نداره ، علت به وجود آمدنش هم ساده است ، برای اینکه هر کسی از ظن خودش یار قضیه نشه و خیال همه راحت باشه که میدونن دارن در مورد چی صحبت می‌کنن و داستان اون فیل در تاریکی هم نشود ، و در ضمن رساندن منظور با عکس خیلی بهتر از یک صفحه نوشته است
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت دوازدهم - توزیع برنامه
- در مورد تنظیمات سمت سرور AngularJS 2.0 در ASP.NET Core 1.0، در قسمت «معرفی بسته‌ی نیوگت Microsoft.AspNetCore.SpaServices» بحث شده‌است.
- اما ... Gulp جاوا اسکریپتی اساسا وابستگی خاصی به فناوری‌های سمت سرور ندارد. در اینجا فقط نحوه‌ی مسیردهی این پوشه‌ها مهم هستند (و Task runner آن فقط به این مسایل دقت می‌کند):
var appFolder = "./app";
var outFolder = "wwwroot";
در اینجا app/. به این معنا است که محل واقع شدن فایل تنظیمات gulp و همچنین فایل systemjs.config.js، یک سطح بالاتر هستند از پوشه‌ی app (جایی که قرار است فایل‌های اصلی از آن دریافت شوند) و همچنین پوشه‌ی wwwroot ذکر شده (جایی که قرار است فایل‌های نهایی کپی شوند):
gulpfile.js
systemjs.config.js
--app
--wwwroot
مطالب
پیاده‌سازی میکروسرویس‌ها توسط Seneca
همانطور که در مطلب آشنایی با معماری Microservices گفته شد، Seneca یک فریم‌ورک مبتنی بر Node.js برای ساخت برنامه‌های سمت سرور بر مبنای معماری Microservices با هسته Monolithic است. در این مطلب قصد ارائه یک مثال عملی را بر اساس این فریم‌ورک ندارم. هدف، آشنایی با اجزای اصلی Seneca و چهارچوب کاری آن است.


فواید استفاده از فریم‌ورک Seneca  

  • فریم‌ورک Seneca کدنویسی برای ایجاد درخواست‌ها، ارسال پاسخ به درخواست‌های رسیده و تبدیل داده‌ها را که از روتین‌های هر سرویسی میتوانند باشند، ساده‌تر میکند.
  • فریم‌ورک Seneca با معرفی ایده Actionها و Pluginها که جلوتر توضیح داده خواهند شد، روند تبدیل کامپوننت‌های یک برنامه Monolithic را به نوع سرویسی، تسهیل میکند. روندی که به صورت عادی میتواند کاری طاقت فرسا باشد و نیاز به Refactoring زیادی دارد. 
  • سرویس‌های نوشته شده با زبانهای برنامه‌نویسی مختلف یا فریم‌ورک‌های متفاوت میتوانند با سرویس‌های نوشته شده توسط فریم‌ورک Seneca در ارتباط و تبادل باشند.


نصب فریم‌ورک Seneca

نصب Seneca تفاوتی با نصب سایر فریم‌ورک‌ها توسط npm ندارد. مثلا میشود فایلی با نام package.json را با تنظیماتی که در پی خواهد آمد داشت و با اجرای دستور npm install در خط فرمان، Seneca را نصب کرد. البته دستور نصب را باید در پوشه‌ای که فایل package.json در آن  ایجاد شده‌است، اجرا کرد.
{
    "name": "seneca-example",
    "dependencies": {
        "seneca": "0.6.5",
        "express": "latest"
    }
}
بعد برای استفاده از فریم‌ورک نصب شده باید نمونه‌ای از آن را ایجاد کنیم. 
var seneca = require("seneca")();

 Actionها

‌Actionها عملکرد بخصوصی را ارائه می‌دهند که باید به نمونه‌ای از فریم‌ورک Seneca که ایجاد کرده‌ایم، اضافه شوند. اینکه یک Action چه عملکرد بخصوصی را ارائه می‌هد، در زمان اضافه کردن آن به نمونه‌ای ایجاد شده، مشخص می‌شود. در مطلب گذشته گفته شد که برای تغییر یک برنامه Monolithic به نوع سرویسی، می‌شود فقط کامپوننت‌های دردسر ساز را تغییر داد. Actionها عملکردهای آن کامپوننت‌های مشکل ساز را به عهده خواهند گرفت. زمان اضافه کردن یک Action به نمونه‌ای از Seneca، از متد add استفاده میکنیم که دو آرگومان را دریافت میکند. اولین آرگومان، یک رشته JSON یا یک object است که هویت عملکرد Action را نشان می‌دهد و دومی در واقع یک متد Callback است و زمانیکه Action فراخوانی میشود، اجرا خواهد شد.  
seneca.add({role: "accountManagement", cmd: "login"}, function(args, respond){
});
seneca.add({role: "accountManagement", cmd: "register"}, function(args, respond){
});
در مثال بالا دو Action را به نمونه‌ای از Seneca اضافه کرده‌ایم. خواص role و cmd موارد بخصوصی نیستند. می‌شود آن‌ها را با  موارد دلخواه، بسته به عملکردی که از Action انتظار داریم، جایگزین کنیم. برای فراخوانی Actionهایی که اضافه کرده‌ایم، باید از متد act استفاده کنیم. البته متد act میتواند Actionهایی را که در سایر سرویس‌ها قرار دارند هم فراخوانی کند. اولین آرگومان act، برای تطبیق با الگوهایی که در زمان اضافه کردن Actionها به نمونه تنظیم شده‌اند، استفاده می‌شود. دومین آرگومان آن هم باز یک Callback است و در زمانیکه Action فراخوانی شده‌است اجرا میشود. 
seneca.act({role: "accountManagement", cmd: "register", username:
"parham", password: "12345!"}, function(error, response){
});

seneca.act({role: "accountManagement", cmd: "login", username:
"parham", password: "12345!"}, function(error, response){
});
در مثال بالا و در پارامتر اول، مشخصه‌های بیشتری نسبت به الگوی Actionهایی که اضافه کرده‌ایم، دیده می‌شود. این مسئله مشکلی ایجاد نمیکند و به هر حال Seneca تمامی الگوهای مشابه را شناسایی میکند و آن موردی که بیشترین تطبیق را دارد، فرا میخواند. 


Pluginها

  Pluginها در Seneca در واقع بسته بندیهایی از Actionهایی هستند که کاربرد مشابهی دارند. در مثال بالا دو Action داشتیم که یکی عملکرد ورود را برعهده دارد و دیگری ثبت‌نام. از آنجایی که هر دو برای بخش مدیریت کاربران تشابهاتی دارند، می‌شود آنها را در یک Plugin بسته بندی کرد. ارزش Pluginها زمانی است که می‌خواهیم آنها را توزیع کنیم. با توزیع Plugin، تمام Actionهای زیرمجموعه آن همزمان توزیع می‌شوند. Pluginها را میشود در قالب توابع یا ماژول‌ها ایجاد کرد.
function account(options){
    this.add({init: "account"}, function(pluginInfo, respond){
        console.log(options.message);
        respond();
    })  

    this.add({role: "accountManagement", cmd: "login"}, function(args, respond){
     }); 

    this.add({role: "accountManagement", cmd: "register"}, function(args, respond){
    });
}

seneca.use(account, {message: "Plugin Added"});
در مثال بالا از روش تابع برای ایجاد Plugin استفاده شده‌است. Plugin را ایجاد می‌کنیم و Actionهایی را که میخواهیم به Plugin اضافه شوند، توسط this اضافه می‌کنیم و در نهایت با متد use به نمونه Seneca میگوییم که از Plugin ساخته شده، استفاده کند. کلمه کلیدی This درون Plugin به نمونه‌ای از Seneca که ایجاد کرده‌ایم، ارجاع دارد. یعنی هر this.add برابر با seneca.add می‌باشد. نکته باقی مانده، Action اضافه‌ای است با مشخصه {init: "account"} که چه نقشی دارد. این Action نقش یک سازنده را برای مقداردهی اولیه، دارد. برای نمونه میشود از آن برای ارتباط با پایگاه داده، پیش از اجرا شدن سایر Actionها که نیاز به آن ارتباط دارند، استفاده کرد. مثال بالا را می‌شود با یک ماژول هم پیاده‌سازی کرد.
module.exports = function(options){
    this.add({init: "account"}, function(pluginInfo, respond){
        console.log(options.message);
        respond();
    })

    this.add({role: "accountManagement", cmd: "login"}, function(args, respond){
    });

    this.add({role: "accountManagement", cmd: "register"},function(args, respond){
    });

    return "account";
}  

seneca.use("./account.js", {message: "Plugin Added"});
 ماژول، نام Plugin را برگشت میدهد که با فرض بر اینکه ماژول در فایل account.js قرار دارد، با متد use به نمونه Seneca میگوییم که از Plugin ساخته شده استفاده کند.


Serviceها  

یک سرویس، یک نمونه از فریم‌ورک Seneca است که Actionهایی را برای استفاده بیرونی، در معرض شبکه قرار می‌دهد. 
var seneca = require("seneca")();
seneca.use("./account.js", {message: "Plugin Added"});
seneca.listen({port: "9090", pin: {role: "accountManagement"}});
در مثال بالا، نمونه‌ای از فریم‌ورک Seneca ایجاد شده، مجموعه‌ای از Actionها تحت یک Plugin به آن اضافه شده و در نهایت Actionهایی که در زمان ساخت و اضافه کردن آنها نقش accountManagement گرفته‌اند، برای استفاده در معرض شبکه و در بستر HTTP قرار می‌گیرند. هر دو بخش port و pin، اختیاری هستند و با صرفنظر از این دو بخش، اولا نمونه‌ی Seneca برای دریافت درخواست‌های ورودی، به درگاه پیش فرض 10101 گوش می‌دهد و ثانیا تمامی Actionهایی را که به نمونه اضافه کرده‌ایم، در معرض شبکه قرار میگیرند.
در سمت دیگر، برای اینکه سایر سرویس‌ها و برنامه‌های Monolithic قادر باشند Actionهای در معرض شکبه قرار داده شده را فراخوانی کنند، باید آنچه را که در معرض قرار داده شده، در خود ثبت کنند. برای مثال سرویس دیگری از Seneca می‌تواند از قطعه کد زیر استفاده کند.
seneca.client({port: "9090", pin: {role: "accountManagement"}});

در اینجا هم موارد port و pin اختیاری هستند. اگر سرویسی که میخواهیم ثبت کنیم در سرور دیگری باشد، بایستی خاصیت host و با مقدار آدرس IP سرور مورد نظر در زمان ثبت، اعمال شود. سوالی که باقی می‌ماند این است که یک سرویس چطور Actionی از یک سرویس دیگر را فراخوانی می‌کند؟

در بخش Actionها آمد که برای فراخوانی یک Action از متد act،  از نمونه‌ی Seneca استفاده میکنیم. رفتار Seneca به این صورت است که ابتدا بر اساس امضای Action درخواست شده، Actionهای محلی را که به سرویس جاری اضافه شده‌اند، جستجو میکند. اگر تطبیقی نیافت به سراغ Actionهای ثبت شده خارجی که دارای خاصیت pin هستند، خواهد رفت و در نهایت اگر آنجا هم موردی نیافت، برای تک تک سرویس‌هایی که آنها را ثبت کرده، اما خاصیت pin را ندارند، درخواستی را ارسال میکند.


برای اطلاعات بیشتر به بخش مستندات  فریم‌ورک Seneca رجوع کنید.

مطالب
lambda expression در Vb.net
با vb.net تو پروژه vs2012 خیلی بیرحمانه رفتار شد. خیلی از برنامه نویسان هنوز فکر میکنن vb.net زبان آموزشه نه برنامه نویسی. حالا با اومدن MVC, EF4, ParallelPrograming, Async و غیره موضوع بدتر شده و  در اکثر وبسایتها معلولا بحث و sampleها با زبان #C ارائه میشه. اکثر Pattern که نوشته میشه به زبان #C. تو MVC4 بوضوع میتونید اینو لمس کنید.
امروز میخوام چندتا مثال از دستورات EF با  lambda expression بنویسم.
دستور Select
Dim query = db.table.Select(Function(q)  q)
Select سفارشی
Dim query = db.table.Select(Function(q) New With{q.field1,q.field2,...})
Select  پردن کردن فیلدهای یک Property
class:
Public Class person
Public Property id As Integet
Public Property fname As String
End Class
entity commmand:
Dim query = db.table.Where(Function(q) New person With{.id=q.idfield,.fname=q.namefield})
دستور where، single
Dim query = db.table.Where(Function(q) q.yourfield = yourvaribale).Select(Function(p) p).Single()
دستور join و group
join:
Dim query = db.table1.Join(db.table2, Function(q) q.youridfield, Function(p) p.youridfield, Function(q, p) New With {Key.q= q, Key.p= p})
group:
Dim query = db.table.GroupBy(Function(q) q.groupkey).Select(Function(p) p.Sum(Function(w) w.aggregate)) 
مابقی دستورات مسه iinsert,delete,update نگذاشتم. اگر دوستان مایل بودن pm بدن این دستوراتم براشون میزارم
موفق باشید
نظرات مطالب
Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت
روش استفاده از TypeScript در پروژه‌های Blazor
شاید علاقمند باشید تا اسکریپت‌های مورد نیاز یک پروژه‌ی Blazor را با TypeScript تهیه کنید؛ تا از مزایای بررسی نوع‌ها، intellisense قوی، null checking و غیره بهره‌مند شوید و سپس توسط کامپایلر آن، حاصل را به کدهای نهایی js تبدیل کنید. برای اینکار می‌توان مراحل زیر را طی کرد:

الف) تهیه فایل تنظیمات کامپایلر TypeScript
نیاز است فایل tsconfig.json را در ریشه‌ی پروژه، جائیکه فایل csproj قرار دارد، با محتوای زیر ایجاد کرد:
{
  "compilerOptions": {
    "strict": true,
    "removeComments": false,
    "sourceMap": false,
    "noEmitOnError": true,
    "target": "ES2020",
    "module": "ES2020",
    "outDir": "wwwroot/scripts"
  },
  "include": [
    "Scripts/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
در این حالت فرض بر این است که فایل‌های ts. در پوشه‌ی scripts قرار گرفته‌اند و فایل‌های نهایی کامپایل شده در پوشه‌ی wwwroot/scripts تولید خواهند شد.

ب) فعالسازی کامپایلر TypeScript به ازای هر بار build برنامه
برای اینکار نیاز است فایل csproj را به صورت زیر تکمیل کرد:
<Project Sdk="Microsoft.NET.Sdk.Razor">
  <ItemGroup>
    <PackageReference Include="Microsoft.TypeScript.MSBuild" Version="4.3.5">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <Content Remove="tsconfig.json" />
  </ItemGroup>
  <ItemGroup>
    <TypeScriptCompile Include="tsconfig.json">
      <CopyToOutputDirectory>Never</CopyToOutputDirectory>
    </TypeScriptCompile>
  </ItemGroup>
</Project>
با اینکار ابزار TypeScript.MSBuild اضافه شده، بر اساس tsconfig.json قسمت الف، کار کامپایل فایل‌های ts را به صورت خودکار انجام می‌دهد.

ج) یک مثال از تبدیل کدهای js به ts
فرض کنید کدهای سراسری زیر را داریم که به شیء window اضافه شده‌اند:
window.exampleJsFunctions = {
  showPrompt: function (message) {
    return prompt(message, 'Type anything here');
  }
};
اکنون برای تبدیل آن به ts.، می‌توان به صورت زیر، فضای نام و کلاسی را ایجاد کرد:
namespace JSInteropWithTypeScript {
   export class ExampleJsFunctions {
        public showPrompt(message: string): string {
            return prompt(message, 'Type anything here');
        }
    }
}

export function showPrompt(message: string): string {
   var fns = new JSInteropWithTypeScript. ExampleJsFunctions();
   return fns.showPrompt(message);
}
قسمت مهم آن، export function انتهایی است. این موردی است که توسط Blazor قابل شناسایی و استفاده است.

د) روش استفاده از خروجی کامپایل شده‌ی TypeScript در کامپوننت‌های Blazor
پس از کامپایل قطعه کد فوق، ابتدا مسیر قابل دسترسی به فایل js قرار گرفته شده در پوشه‌ی wwwroot را مشخص می‌کنیم که همواره با الگوی زیر است. همچنین اینبار IJSObjectReference است که امکان دسترسی به export function یاد شده را میسر می‌کند:
private const string ScriptPath = "./_content/----namespace-here---/scripts/file.js";
private IJSObjectReference scriptModule;
دو تعریف فوق، فیلدهایی هستند که در سطح کامپوننت تعریف می‌شوند. سپس مقدار دهی آن‌ها در OnAfterRenderAsync صورت می‌گیرد تا کار import ماژول را انجام دهد:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
  if (scriptModule == null)
    scriptModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", ScriptPath);
پس از این مرحله، امکان کار با ماژول بارگذاری شده، به صورت متداولی میسر می‌شود و می‌توان export function‌ها را در اینجا فراخوانی کرد:
await scriptModule.InvokeVoidAsync("exported fn name", params);
در آخر کار هم باید آن‌را dispose کرد؛ که روش آن به صورت زیر است:
- ابتدا باید این کامپوننت، IAsyncDisposable را پیاده سازی کند:
public partial class MyComponent : IAsyncDisposable
سپس پیاده سازی آن به صورت زیر انجام می‌شود:
public async ValueTask DisposeAsync()
{
  if (scriptModule != null)
  {
    await scriptModule.DisposeAsync();
  }
}
مطالب
مسیریابی در Angular - قسمت نهم - محافظ‌های مسیرها
جهت مقاصد امنیتی، اعتبارسنجی کاربران و یا تحت نظر قرار دادن مسیرها، نیاز است بتوان بررسی کرد که آیا پیمایش یک مسیر، مجاز است یا خیر؟ برای پیاده سازی یک چنین ویژگی‌هایی در Angular، مفهوم Route Guards یا محافظ‌های مسیرها پیش بینی شده‌است که شامل چندین نوع محافظ می‌شوند:
 - canActivate : جهت محافظت دسترسی به یک مسیر
 -  canActivateChild: برای محافظت دسترسی به یک Child Route
 - canDeactivate : برای جلوگیری کردن از ترک مسیر جاری و هدایت به مسیری دیگر (برای مثال جهت نمایش پیام «هنوز اطلاع تغییر یافته را ذخیره نکرده‌اید»)
 - canLoad : برای جلوگیری از مسیریابی غیرهمزمان (async routing) که در قسمت بعدی بررسی خواهد شد
 - resolve: برای پیش واکشی اطلاعات، پیش از نمایش مسیر (که آن‌را در قسمت چهارم این سری بررسی کردیم)


لزوم استفاده‌ی از محافظ‌های مسیرها


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


ترتیب اجرای محافظ‌های مسیرها

مسیریاب سیستم، ابتدا محافظ canDeactivate را اجرا می‌کند تا مشخص شود که آیا کاربر می‌تواند مسیر جاری را ترک کند یا خیر؟ سپس اگر مسیریابی تعریف شده غیرهمزمان باشد، محافظ canLoad اجرا می‌شود. پس از آن محافظ canActivateChild بررسی می‌شود. در ادامه محافظ canActivate اجرا می‌گردد. در پایان کار بررسی محافظ‌های موجود، کار بررسی محافظ resolve‌، جهت پیش واکشی اطلاعات مسیر درخواستی، انجام خواهد شد.
در اینجا اگر یکی از محافظ‌ها مقدار false را برگرداند، پردازش مابقی آن‌ها لغو خواهد شد و کار هدایت کاربر به مسیر درخواستی، خاتمه می‌یابد.


مراحل ساخت و اعمال یک محافظ مسیر

ساخت و اعمال یک محافظ مسیر شامل سه مرحله است:
الف) یک محافظ مسیر عموما به صورت یک سرویس جدید پیاده سازی می‌شود:
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {

    }
}
در اینجا برای اینکه این سرویس به صورت یک محافظ مسیر عمل کند، نیاز است نوع محافظ مدنظر را نیز پیاده سازی نماید؛ مانند CanActivate در اینجا. پس از آن باید متد مرتبط با این اینترفیس که در اینجا canActivate است، پیاده سازی شود. اگر این متد false را برگرداند، سبب لغو هدایت کاربر به آن مسیر خواهد شد و این متد می‌تواند خروجی پیچیده‌تری مانند یک Observable را نیز داشته باشد. اگر یک چنین نوع خروجی درنظر گرفته شود، فراخوان آن، تا پایان کار این Observable صبر خواهد کرد.

ب) از آنجائیکه محافظ‌ها، سرویس هستند، نیاز است تعریف کلاس آن‌ها را در قسمت providers ماژول مرتبط نیز ذکر کنیم تا در برنامه قابل دسترسی شوند. باید دقت داشت که برخلاف سایر سرویس‌ها، امکان تعریف محافظ‌ها صرفا در سطح یک ماژول مسیر است و نه در سطح یک کامپوننت. به این ترتیب مسیریاب می‌تواند به آن، در طی هدایت کاربر به مسیر درخواستی، دسترسی پیدا کند.

ج) پس از آن برای فعالسازی یک محافظ مسیر، آن‌را به عنوان یک خاصیت جدید، به تنظیمات مسیریابی اضافه خواهیم کرد. نام این خاصیت دقیقا مساوی با نوع محافظی است که تعریف شده‌است. برای مثال اگر محافظ تعریف شده از نوع CanActivate است، نام خاصیتی که ذکر خواهد شد، canActivate می‌باشد. مقدار آن نیز می‌تواند آرایه‌ای از سرویس‌هایی از این نوع باشد.

امکان به اشتراک گذاشتن یک محافظ بین چندین مسیر نیز وجود دارد. فرض کنید می‌خواهیم تمام مسیرهای مربوط به محصولات را محافظت کنیم. در این حالت می‌توان محافظ را به تک تک Child routes موجود اعمال کرد و یا می‌توان محافظ را به والد آن‌ها نیز اعمال کنیم تا به صورت خودکار سبب محافظت از فرزندان آن نیز شویم.


یک مثال: ساخت محافظ canActivate‌

جهت بررسی شرط یا شرایطی پیش از فعال سازی یک مسیر درخواستی، از محافظ‌هایی از نوع canActivate می‌توان استفاده کرد. این نوع محافظ‌ها عموما جهت اعتبارسنجی کاربران و محدود سازی دسترسی آن‌ها به قسمت‌های مختلف برنامه استفاده می‌شوند. این نوع محافظ‌ها حتی با تغییر پارامترهای مسیریابی نیز فعال شده و بررسی می‌شوند.

در ادامه‌ی مثال این سری می‌خواهیم کاربران را پیش از دسترسی به قسمت‌های مختلف مرتبط با محصولات، وادار به لاگین کنیم. برای این منظور دستور ذیل را اجرا کنید:
 >ng g guard user/auth -m user/user.module
به این ترتیب تغییرات ذیل در ماژول کاربران رخ خواهند داد:
 installing guard
  create src\app\user\auth.guard.spec.ts
  create src\app\user\auth.guard.ts
  update src\app\user\user.module.ts
در اینجا قالب ابتدایی کلاس سرویس AuthGuard ایجاد می‌شود (در فایل auth.guard.ts) و همچنین اگر به سطر آخر آن دقت کنید، این سرویس را به قسمت providers ماژول کاربران (در فایل user.module.ts) نیز افزوده‌است.

در ادامه کدهای این محافظ را به صورت ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router } from '@angular/router';

import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService,
    private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.checkLoggedIn(state.url);
  }

  checkLoggedIn(url: string): boolean {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}
خاصیت redirectUrl نیز به کلاس سرویسAuthService ، جهت به اشتراک گذاری اطلاعات، اضافه شده‌است:
export class AuthService {
   currentUser: IUser;
   redirectUrl: string;

توضیحات:

این سرویس چون از نوع CanActivate است، این اینترفیس را پیاده سازی کرده‌است و همچنین متد canActivate آن‌را نیز به همراه دارد:
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
در اینجا از ActivatedRouteSnapshot می‌توان اطلاعات مسیرجاری، مانند پارامترهای آن‌را بدست آورد. پارامتر RouterStateSnapshot نیز وضعیت مسیریابی را بازگشت می‌دهد. برای مثال state.url، حاوی آدرس کامل مسیر درخواستی به صورت یک رشته است که از آن در اینجا جهت حفظ و به اشتراک گذاری مسیر اولیه‌ی درخواستی استفاده شده‌است. خاصیت route.url حاوی آرایه‌ای از URL segments است.

یک نکته: هرچند در اینجا می‌توان به پارامتر id مسیر، مانند route.params['id'] در صورت نیاز دسترسی یافت، اما امکان دسترسی به اطلاعات از پیش واکشی شده مانند route.data['product'] وجود ندارد. علت آن‌را نیز در قسمت «ترتیب اجرای محافظ‌های مسیرها» ابتدای بحث جاری، بررسی کردیم: محافظ resolve در انتهای کار پردازش تمام محافظ‌های موجود فراخوانی می‌شود.

در متد canActivate می‌خواهیم بررسی کنیم که آیا کاربر، لاگین کرده‌است یا خیر؟ اگر بله، تنها کافی است true را بازگشت دهیم تا کار این محافظ پایان یابد. در غیراینصورت false را بازگشت داده و همچنین سبب هدایت کاربر به صفحه‌ی لاگین می‌شویم.
به همین منظور سرویس AuthService را به سازنده‌ی این کلاس تزریق کرده‌ایم تا بتوانیم به متد isLoggedIn آن دسترسی پیدا کنیم (این سرویس را در قسمت دوم این سری تکمیل کردیم).
این متد نیز به صورت ذیل تعریف شده‌است:
isLoggedIn(): boolean {
   return !this.currentUser;
}
در اینجا استفاده‌ی از ! سبب بازگشت true، در صورت نال نبودن شیء کاربر جاری وارد شده‌ی به سیستم می‌شود.

در ادامه برای استفاده‌ی از این محافظ مسیر، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آن‌را به نحو ذیل اعمال خواهیم کرد:
import { AuthGuard } from './../user/auth.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],
    children: [    ]
  }
];
در قسمت ششم، کار گروه بندی مسیرها را انجام دادیم. اکنون در اینجا نمونه‌ای از استفاده‌ی از آن‌را مشاهده می‌کنید. بجای اینکه AuthGuard  را به تک تک مسیرهای فرزند تعریف شده‌ی محصولات، اعمال کنیم، آن‌را به والد این مسیر اعمال کرده‌ایم تا به صورت خودکار به تمام فرزندان آن نیز اعمال شود.

اکنون برنامه را با دستور ng s -o ساخته و اجرا کنید. سپس بر روی لینک لیست محصولات و یا افزودن یک محصول جدید کلیک کنید. بلافاصله صفحه‌ی لاگین را مشاهده خواهید کرد.


به خاطر سپاری و بازیابی مسیر درخواستی کاربر پس از لاگین

در اینجا اگر کاربر بر روی لینک افزودن یک محصول جدید کلیک کند، صفحه‌ی لاگین را مشاهده خواهد کرد. اما پس از لاگین، همواره به مسیر لیست محصولات هدایت می‌شود و در این حالت مسیر درخواستی اولیه فراموش خواهد شد. برای رفع این مشکل نیاز است آدرس درخواستی کاربر را نیز ذخیره و بازیابی کرد. به همین جهت خاصیت this.authService.redirectUrl = url را در متد checkLoggedIn محافظ تعریف شده مقدار دهی کردیم. در اینجا از سرویس Auth، برای به اشتراک گذاری اطلاعات با محافظ‌های مسیر استفاده کرده‌ایم. طول عمر یک سرویس، singleton است. بنابراین تنها یک وهله از آن در طول عمر برنامه وجود خواهد داشت. به این ترتیب با ذخیره‌ی اطلاعاتی در آن، این اطلاعات در تمام برنامه قابل دسترسی خواهد شد.
با توجه به این نکته، اکنون به فایل src\app\user\login\login.component.ts مراجعه کرده و قسمت this.router.navigate آن‌را به صورت ذیل بهبود خواهیم بخشید:
      if (this.authService.login(userName, password)) {
        if (this.authService.redirectUrl) {
          this.router.navigateByUrl(this.authService.redirectUrl);
        } else {
          this.router.navigate(['/products']);
        }
      }
در اینجا بررسی می‌شود که آیا پیشتر خاصیت redirectUrl پس از لاگین مقدار دهی شده‌است یا خیر؟ اگر بله، از متد navigateByUrl جهت هدایت به آن مسیر استفاده خواهد شد.

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


بررسی محافظ canActivateChild

این محافظ نیز شبیه به محافظ canActivate است؛ با این تفاوت که تنها زمانی فعالسازی خواهد شد که فرزند یک مسیر قرار است نمایش داده شود و نه خود مسیر اصلی.
محافظ canActivateChild با تغییر قسمت child یک مسیر فعالسازی می‌شود؛ حتی اگر این تغییر در حد تغییر پارامترهای آن مسیر باشد. اما باید درنظر داشت که اگر تنها قسمت child یک مسیر تغییر کند، دیگر محافظ canActivate مجددا اجرا نخواهد شد.

یک مثال: اگر کاربر در حال مشاهده‌ی صفحه‌ی لیست محصولات باشد و بر روی لینک مشاهده‌ی یک محصول کلیک کند، تنها قسمت child مسیر تغییر می‌کند. در این حالت canActivate مسیر اصلی دیگر اجرا نخواهد شد؛ اما تمام محافظ‌های canActivateChild مرتبط مجددا اجرا خواهند شد.


بررسی محافظ canDeactivate

محافظ canDeactivate پیش از ترک یک مسیر، فعالسازی و بررسی می‌شود. عموما از آن جهت بررسی وضعیت اطلاعات ذخیره نشده و اطلاع رسانی به کاربر، پیش از ترک مسیر جاری استفاده استفاده می‌گردد. این محافظ با هر تغییری در آدرس جاری مسیر، بررسی می‌شود. بدیهی است این تغییر صرفا درون یک برنامه‌ی Angular معنا پیدا می‌کند و نه هدایت به سایتی دیگر.
در حال حاضر در مثال جاری این سری، اگر کاربر، تغییری را در صفحه‌ی ویرایش اطلاعات ایجاد کند و بدون کلیک بر روی دکمه‌ی Save به صفحه‌ی دیگری مراجعه کند، این اطلاعات تغییر یافته، از دست خواهند رفت. برای رفع این مشکل می‌توان محافظ canDeactivate ایی را برای آن طراحی کرد. به همین جهت دستور ذیل را اجرا کنید:
 >ng g guard product/ProductEdit -m product/product.module
تا سبب انجام تغییرات ذیل در ماژول محصولات شود:
 installing guard
  create src\app\product\product-edit.guard.spec.ts
  create src\app\product\product-edit.guard.ts
  update src\app\product\product.module.ts
در اینجا علاوه بر ایجاد قالب ابتدایی محافظ ProductEdit، سبب به روز رسانی قسمت providers ماژول محصولات نیز شده‌است.

امضای ابتدایی یک محافظ CanDeactivate به صورت ذیل است:
export  class ProductEditGuard implements CanDeactivate<ProductEditComponent> {
    canDeactivate(component: ProductEditComponent): boolean {
اینترفیس CanDeactivate جنریک بوده و پارامتر جنریک آن نوع کامپوننتی را که قرار است از این محافظ استفاده کند، مشخص می‌کند. سپس نوع پارامتر متد canDeactivate آن بر اساس نوع پارامتر جنریک، تعیین می‌گردد.
اکنون این محافظ نیاز دارد تا بداند که آیا کامپوننت ویرایش محصولات، دارای اطلاعات ذخیره نشده‌ای هست یا خیر؟ چون کامپوننت ویرایش محصولات، به عنوان پارامتر به متد canDeactivate آن ارسال شده‌است، بنابراین می‌تواند به خواص و متد‌های عمومی آن کلاس نیز دسترسی پیدا کند. به همین جهت تغییرات ذیل را به کامپوننت ویرایش محصولات در فایل src\app\product\product-edit\product-edit.component.ts اعمال می‌کنیم:
  get product(): IProduct {
    return this.currentProduct;
  }
  set product(value: IProduct) {
    this.currentProduct = value;
    // Clone the object to retain a copy
    this.originalProduct = Object.assign({}, value);
  }

  get isDirty(): boolean {
    return JSON.stringify(this.originalProduct) !== JSON.stringify(this.currentProduct);
  }
در اینجا یک کپی از اصل محصول در حال ویرایش، برای مقایسه‌ی آن با محصول جاری در حال ویرایش، نگهداری می‌شود. به این ترتیب خاصیت isDirty می‌تواند مشخص کند که آیا تغییری بر روی خواص این شیء صورت گرفته‌است یا خیر؟ استفاده از متد JSON.stringify، یکی از ساده‌ترین روش‌هایی است که از آن می‌توان جهت مقایسه‌ی تمام خواص دو شیء استفاده کرد. البته چون در اینجا ترتیب خواص این دو شیء یکی است، این روش کار می‌کند.
برای اینکه این امر میسر شود، خاصیت product به حالت get/set دار تغییر یافته‌است تا بتوان کپی اولیه‌ی محصول را جهت مقایسه، نگهداری کرد. استفاده از متد Object.assign سبب ایجاد یک کپی از شیء اولیه شده و به این صورت دو وهله‌ی غیرمشترک را خواهیم داشت. اگر value مستقیما به originalProduct  انتساب داده می‌شد، در این حالت هر دوی currentProduct و originalProduct به یک شیء اشاره می‌کردند.

اکنون می‌توان از این خاصیت جدید کامپوننت ویرایش محصولات، در محافظ ترک صفحه‌ی آن استفاده کرد:
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';

import { ProductEditComponent } from './product-edit/product-edit.component';

@Injectable()
export class ProductEditGuard implements CanDeactivate<ProductEditComponent> {

  canDeactivate(component: ProductEditComponent): boolean {
    if (component.isDirty) {
      let productName = component.product.productName || 'New Product';
      return confirm(`Navigate away and lose all changes to ${productName}?`);
    }
    return true;
  }
}
در اینجا اگر فرم، تغییر یافته و هنوز ذخیره نشده باشد، خاصیت isDirty برقرار شده و سبب نمایش یک دیالوگ confirm می‌شود. اگر کاربر آن‌را تائید کند، آنگاه مسیر درخواستی جدید فعال می‌شود. در غیراینصورت، هدایت به مسیر جدید لغو خواهد شد.

در آخر برای استفاده‌ی از این محافظ جدید، باید آن‌را به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت به فایل src\app\product\product-routing.module.ts مراجعه کرده و این محافظ را به والد مسیریابی ویرایش یک محصول اضافه می‌کنیم:
import { ProductEditGuard } from './product-edit.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],    
    children: [
      {
        path: '',
        component: ProductListComponent
      },
      {
        path: ':id',
        component: ProductDetailComponent,
        resolve: { product: ProductResolverService }
      },
      {
        path: ':id/edit',
        component: ProductEditComponent,
        resolve: { product: ProductResolverService },
        canDeactivate: [ ProductEditGuard ],
        children: [
          { path: '', redirectTo: 'info', pathMatch: 'full' },
          { path: 'info', component: ProductEditInfoComponent },
          { path: 'tags', component: ProductEditTagsComponent }
        ]
      }
    ]
  }
];
با افزودن canDeactivate به والد ویرایش محصولات، از هر دو child route تعریف شده محافظت می‌کند.


برای آزمایش آن، به صفحه‌ی ویرایش یکی از محصولات مراجعه کرده و تغییری را ایجاد کنید. سپس درخواست مشاهده‌ی صفحه‌ی دیگری را با کلیک بر روی یکی از لینک‌های منوی برنامه ارائه دهید. بلافاصله دیالوگ confirm ظاهر خواهد شد (تصویر فوق).

مشکل! در همین حالت بر روی دکمه‌ی Ok کلیک کنید تا اطلاعات ذخیره نشده را از دست داده و به مسیر دیگری هدایت شویم. مجددا همین پروسه را تکرار کنید. اینبار اگر بر روی دکمه‌ی Save کلیک کنید، باز هم دیالوگ confirm ظاهر می‌شود. علت اینجا است که شیء محصول اصلی و جاری، پس از ذخیره سازی به حالت اولیه بازگشت داده نشده‌اند. برای این منظور متد reset را به کامپوننت ویرایش اطلاعات اضافه کرده:
reset(): void {
    this.dataIsValid = null;
    this.currentProduct = null;
    this.originalProduct = null;
  }
و سپس آن‌را به متد onSaveComplete، اضافه می‌کنیم:
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }
    this.reset();

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.
نظرات مطالب
مشکل اعتبار سنجی jQuery validator در Bootstrap tabs
نکته تکمیلی
به صورت پیش فرض امکان مطلع شدن از خطاهای اعتبارسنجی درtabهای دیگر به غیر از tab جاری، ممکن نمی‌باشد؛ برای حل این مسئله می‌توان به شکل زیر عمل کرد:
 if ($(".tab-content").find("div.tab-pane.active:has(div.has-error)").length === 0) {
            $(".tab-content").find("div.tab-pane:hidden:has(div.has-error)").each(function (index, tab) {
                var id = $(tab).attr("id");
                $('a[href="#' + id + '"]').tab('show');
            });
        }
در تکه کد بالا ابتدا بررسی می‌شود در tab جاری هیچ خطای اعتبارسنجی وجود نداشته باشد؛ سپس به صورت خودکار tab ای که دارای خطای اعتبارسنجی می‌باشد، فعال خواهد شد.
اگر از unobtrusive validation استفاده نمی‌کنید، می‌توانید به شکل زیر عمل کنید:
$.validator.setDefaults({
    //برای رفع مشکل عدم اعتبارسنجی المان‌های مخفی مانند تب‌های بسته
    ignore: [],
    invalidHandler: function (event, validator) {
        if ($(".tab-content").find("div.tab-pane.active:has(div.has-error)").length === 0) {
            $(".tab-content").find("div.tab-pane:hidden:has(div.has-error)").each(function (index, tab) {
                var id = $(tab).attr("id");
                $('a[href="#' + id + '"]').tab('show');
            });
        }
    }
});
invalidHandler زمانی که فرم دارای خطاهای اعتبارسنجی می‌باشد و سعی داریم آن را به سمت سرور ارسال کنیم (این اتفاق رخ نمی‌دهد)، فراخوانی می‌شود.
در غیر این صورت لازم است، مشترک رویداد «invalid-form.validate» شوید؛ به شکل زیر:
$("form").unbind("invalid-form.validate"); // unbind default MS handler
$("form").bind("invalid-form.validate", function(event , validator) {
    if ($(".tab-content").find("div.tab-pane.active:has(div.has-error)").length === 0) {
        $(".tab-content").find("div.tab-pane:hidden:has(div.has-error)").each(function (index, tab) {
            var id = $(tab).attr("id");
            $('a[href="#' + id + '"]').tab('show');
        });
    }
});