مطالب
آشنایی با مفاهیم شیء گرایی در جاوا اسکریپت #2
از آنجا که برای کار با جاوا اسکریپت نیاز به درک کاملی درباره‌ی مفهوم حوزه کارکرد متغیرها (Scope) می‌باشد و نحوه فراخوانی توابع نیز نقش اساسی در این مورد بازی می‌کند، در این قسمت با این موارد آشنا خواهیم شد:
جاوا اسکریپت از مفهومی به نام functional scope برای تعیین حوزه متغیرها استفاده می‌کند و به این معنی است که با تعریف توابع، حوزه عملکرد متغیر مشخص می‌شود. در واقع هر متغیری که در یک تابع تعریف می‌شود در کلیه قسمتهای آن تابع، از قبیل If statement – for loops و حتی nested function نیز در دسترس میباشد.
اجازه دهید با مثالی این موضوع را بررسی نماییم.
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
همانگونه که میبینیم با اینکه در داخل بلاک if یک متغیر جدید تعریف شده، ولی در خارج از این بلاک نیز این متغیر قابل دسترسی میباشد. البته در مثال بالا اگر بخواهیم به متغیر myTest در خارج از function دسترسی داشته باشیم، با خطای undefined مواجه خواهیم شد. یعنی برای مثال در کد زیر:
function testScope() {
var myTest = true;
if (true) {
var myTest = "I am changed!"
}
alert(myTest);
}
testScope(); // will alert "I am changed!"
alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
 برای حل این مشکل دو راه وجود دارد: 
1 – متغیر myTest را در بیرون بلاک testScope() تعریف کنیم
2 – هنگام تعریف متغیر myTest، کلمه کلیدی var را حذف کنیم که این موضوع باعث میشود این متغیر در کل window قابل دسترس باشد و یا به عبارتی متغیر global میشود.
قبل از پرداختن به ادامه بحث خواندن مقاله مربوط به Closure در جاوااسکریپت توصیه میگردد .
در پایان بحث Scope‌ها با یک مثال نسبتا جامع اکثر این حالات به همراه خروجی را نشان میدهیم :
<script type="text/javascript">
          // a globally-scoped variable
        var a = 1;
        // global scope
        function one()
        {
            alert(a);
        }
        // local scope
        function two(a)
        {
            alert(a);
        }
        // local scope again
        function three()
        {
            var a = 3;
            alert(a);
        }
        // Intermediate: no such thing as block scope in javascript
        function four()
        {
            if (true)
            {
                var a = 4;
            }
            alert(a); // alerts '4', not the global value of '1'
        }
        // Intermediate: object properties
        function Five()
        {
            this.a = 5;
        }
        // Advanced: closure
        var six = function ()
        {
            var foo = 6;
            return function ()
            {
                // javascript "closure" means I have access to foo in here, 
                // because it is defined in the function in which I was defined.
                alert(foo);
            }
        }()
        // Advanced: prototype-based scope resolution
        function Seven()
        {
            this.a = 7;
        }
        // [object].prototype.property loses to [object].property in the lookup chain
        Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above.
        Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor.
        // These will print 1-8
        one();
        two(2);
        three();
        four();
        alert(new Five().a);
        six();
        alert(new Seven().a);
        alert(new Seven().b);
</Script>
برای مطالعه بیشتر به اینجا  مراجعه نمایید.

Function Invocation Patterns In JavaScript :
از آنجا که توابع در جاوااسکریپت به منظور 1 – ساخت اشیاء  و 2 – حوزه دسترسی متغیرها(Scope)  نقش اساسی ایفا می‌کنند بهتر است کمی درباره استفاده و نحوه فراخوانی آنها  (Function Invocation Patterns) در جاوااسکریپت بحث نماییم.
در جاوااسکریپت 4 مدل فراخوانی تابع داریم که به نامهای زیر مطرح هستند:
1. Method Invocation
2. Function Invocation
3. Constructor Invocation
4. Apply And Call Invocation
 در فراخوانی توابع به هر یک از روشهای بالا باید به این نکته توجه داشت که حوزه دسترسی متغیرها در جاوااسکریپت ابتدا و انتهای توابع هستند و اگر به عنوان مثال از توابع تو در تو استفاده کردیم ،حوزه شی this برای توابع داخلی تغییر خواهد کرد .این موضوع را در طی مثالهایی نشان خواهیم داد.
Method Invocation :
وقتی یک تابع قسمتی از یک شی باشد به آن متد میگوییم به عنوان مثال :
var obj = {
    value: 0,
    increment: function() {
        this.value+=1;
    }
};
obj.increment(); //Method invocation
در اینحالت this به شی (Object) اشاره میکند که متد در آن فراخوانی شده است و در زمان اجرا نیز به عناصر شی Bind میشود ،در مثال بالا حوزه  this شی obj خواهد شد و به همین منظور به متغیر value دسترسی داریم.
Function Invocation:
در اینحالت که از () برای فراخوانی تابع استفاده میگردد ،This به شی سراسری (global object ) اشاره می‌کند؛ منظور اینکه this به اجزای تابعی که فراخوانی آن انجام شده اشاره نمی‌کند. اجازه دهید با مثالی این موضوع را روشن کنیم
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        this.value++;
        var innerFunction = function() {
            alert(this.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment(); //Method invocation pattern
<script type="text/javascript">
Result : 500
از آنجا که  () innerFunction به شکل  Function invocation pattern فراخوانی شده است به متغیر value در داخل تابع increment دسترسی نداریم و حوزه دسترسی global میشود و اگر در حوزه global نیز این متغیر تعریف نشده بود به خطای undefined میرسیدیم .
برای حل این گونه مشکلات ساختار کد نویسی ما بایستی به شکل زیر باشد :
<script type="text/javascript">
var value = 500; //Global variable
var obj = {
    value: 0,
    increment: function() {
        var that = this;
        that.value++;
        var innerFunction = function() {
            alert(that.value);
        }
        innerFunction(); //Function invocation pattern
    }
}
obj.increment();
<script type="text/javascript">
Result : 1
در واقع با تعریف یک متغیر با نام مثلا that و انتساب شی  this به آن میتوان در توابع بعدی که به شکل   Function invocation pattern فراخوانی میگردند به این متغیر دسترسی داشت .
Constructor Invocation :
در این روش برای فراخوانی تابع از کلمه new استفاده میکنیم. در این حالت یک شیء مجزا ایجاد شده و به متغیر دلخواه ما اختصاص پیدا می‌کند. به عنوان مثال داریم :
 var Dog = function(name) {   
  //this == brand new object ({});    
    this.name = name;    
    this.age = (Math.random() * 5) + 1;
};
var myDog = new Dog('Spike');
//myDog.name == 'Spike'
//myDog.age == 2
var yourDog = new Dog('Spot');
//yourDog.name == 'Spot'
//yourDog.age == 4
در این مورد با استفاده از New باعث میشویم همه خواص و متدهای تابع function برای هر نمونه از آن که ساخته میشود ( از طریق مفهوم Prototype که قبلا درباره آن بحث شد) بطور مجزا اختصاص یابد. در مثال بالا شی mydog چون حاوی یک نمونه از تابع dog بصورت  Constructor Invocation میباشد، در نتیجه به خواص تابع dog از قبیل name  و age دسترسی داریم. در اینجا اگر کلمه new استفاده نشود به این خواص دسترسی نداریم؛ در واقع با اینکار، this به mydog اختصاص پیدا میکند.
اگر از new استفاده نشود متغیر myDog ،undefined میشود.
یک مثال دیگر :
var createCallBack = function(init) { //First function
    return new function() { //Second function by Constructor Invocation
        var that = this;
        this.message = init;
        return function() { //Third function
            alert(that.message);
        }
    }
}
window.addEventListener('load', createCallBack("First Message"));
window.addEventListener('load', createCallBack("Second Message"));
در مثال بالا از مفهوم closure  نیز در مثالمان استفاده کرده ایم .
Apply And Call Invocation:
تمامی توابع جاوااسکریپت دارای دو متد توکار apply() و call() هستند که توسط این متدها میتوان این توابع را با context دلخواه فراخوانی کرد.
نحوه فراخوانی به شکل مقابل است :
myFunction.apply(thisContext, arrArgs);
myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
که thisContext به حوزه اجرایی (execution context) تابع اشاره میکند. تفاوت دو متد apply() و call() در نحوه فرستادن آرگومانها به تابع میباشد که در اولی توسط آرایه اینکار انجام میشود و در دومی همه آرگومانها را بطور صریح نوشته و با کاما از هم جدا میکنیم .
مثال :
var contextObject = {
testContext: 10
}
var otherContextObject = {
testContext: "Hello World!"
}
var testContext = 15; // Global variable
function testFunction() {
alert(this.testContext);
}
testFunction(); // This will alert 15
testFunction.call(contextObject); // Will alert 10
testFunction.apply(otherContextObject); // Will alert "Hello World”
در این مثال دو شی متفاوت با خواص همنام تعریف کرده و یک متغیر global نیز تعریف میکنیم. در انتها یک تابع تعریف میکنیم که مقدار this.testContext را نمایش میدهد. در ابتدا حوزه اجرایی تابع (this) کل window جاری میباشد و وقتی testFunction() اجرا شود مقدار متغیر global نمایش داده میشود. در اجرای دوم this به contextObject اشاره کرده و حوزه اجرایی عوض میشود و در نتیجه مقدار testContext مربوطه که در این حالت 10 میباشد نمایش داده میشود و برای فراخوانی سوم نیز به همین شکل .
یک مثال کاملتر :
var o = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a();
    document.write(this.i);
  }
};
o.F();
Result :0
خط o.f() تابع f را به شکل Method invocation اجرا میکند. در داخل تابع f یک تابع دیگر به شکل function invocation اجرا میشود که در اینحال this به global object اشاره میکند و باعث میشود مقدار i در خروجی 0 چاپ شود .
برای حل این مشکل 2 راه وجود دارد  
راه اول :
var p = {
  i : 0,
  F : function() {
    var a = function() { this.i = 42; };
    a.apply(this);
    document.write(this.i);
  }
};
 p.F();
Result :42
با اینکار this را موقع اجرای تابع درونی برایش فرستاده تا حوزه اجرای تابع عوض شود و به i دسترسی پیدا کنیم .
یا اینکه همانند مثالهای قبلی :
var q = {
  i: 0,
  F: function F() {
    var that = this;
    var a = function () {
      that.i = 42;
    }
    a();
    document.write(this.i);
  }
}
 q.F();

منابع :
Javascript programmer,s refrence
 
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت ششم - کار با User Claims
از Claims جهت ارائه‌ی اطلاعات مرتبط با هویت هر کاربر و همچنین Authorization استفاده می‌شود. برای مثال درنگارش‌های قبلی ASP.NET، مفاهیمی مانند «نقش‌ها» وجود دارند. در نگارش‌های جدیدتر آن، «نقش‌ها» تنها یکی از انواع «User Claims» هستند. در قسمت‌های قبل روش تعریف این Claims را در IDP و همچنین تنظیمات مورد نیاز جهت دریافت آن‌ها را در سمت برنامه‌ی Mvc Client بررسی کردیم. در اینجا مطالب تکمیلی کار با User Claims را مرور خواهیم کرد.


بررسی Claims Transformations

می‌خواهیم Claims بازگشت داده شده‌ی توسط IDP را به یکسری Claims که کار کردن با آن‌ها در برنامه‌ی MVC Client ساده‌تر است، تبدیل کنیم.
زمانیکه اطلاعات Claim، توسط میان‌افزار oidc دریافت می‌شود، ابتدا بررسی می‌شود که آیا دیکشنری نگاشت‌ها وجود دارد یا خیر؟ اگر بله، کار نگاشت‌ها از یک claim type به claim type دیگر انجام می‌شود.
برای مثال لیست claims اصلی بازگشت داده شده‌ی توسط IDP، پس از تبدیلات و نگاشت‌های آن در برنامه‌ی کلاینت، یک چنین شکلی را پیدا می‌کند:
Claim type: sid - Claim value: f3940d6e58cbb576669ee49c90e22cb1
Claim type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
Claim type: http://schemas.microsoft.com/identity/claims/identityprovider - Claim value: local
Claim type: http://schemas.microsoft.com/claims/authnmethodsreferences - Claim value: pwd
Claim type: given_name - Claim value: Vahid
Claim type: family_name - Claim value: N
در ادامه می‌خواهیم نوع‌های آن‌ها را ساده‌تر کنیم و آن‌ها را دقیقا تبدیل به همان claim typeهایی کنیم که در سمت IDP تنظیم شده‌اند. برای این منظور به فایل src\MvcClient\ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده و سازنده‌ی آن‌را به صورت زیر تغییر می‌دهیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
        }
در اینجا جدول نگاشت‌های پیش‌فرض Claims بازگشت داده شده‌ی توسط IDP به Claims سمت کلاینت، پاک می‌شود.
در ادامه اگر مجددا لیست claims را پس از logout و login، بررسی کنیم، به این صورت تبدیل شده‌است:
• Claim type: sid - Claim value: 91f5a09da5cdbbe18762526da1b996fb
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: idp - Claim value: local
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N
اکنون این نوع‌ها دقیقا با آن‌چیزی که IDP ارائه می‌دهد، تطابق دارند.


کار با مجموعه‌ی User Claims

تا اینجا لیست this.User.Claims، به همراه تعدادی Claims است که به آن‌ها نیازی نداریم؛ مانند sid که بیانگر session id در سمت IDP است و یا idp به معنای identity provider می‌باشد. حذف آن‌ها حجم کوکی نگهداری کننده‌ی آن‌ها را کاهش می‌دهد. همچنین می‌خواهیم تعدادی دیگر را نیز به آن‌ها اضافه کنیم.
علاوه بر این‌ها میان‌افزار oidc، یکسری از claims دریافتی را راسا فیلتر و حذف می‌کند؛ مانند زمان منقضی شدن توکن و امثال آن که در عمل واقعا به تعدادی از آن‌ها نیازی نداریم. اما می‌توان این سطح تصمیم گیری فیلتر claims رسیده را نیز کنترل کرد. در تنظیمات متد AddOpenIdConnect، خاصیت options.ClaimActions نیز وجود دارد که توسط آن می‌توان بر روی حذف و یا افزوده شدن Claims، کنترل بیشتری را اعمال کرد:
namespace ImageGallery.MvcClient.WebApp
{
        public void ConfigureServices(IServiceCollection services)
        {
// ...
              .AddOpenIdConnect("oidc", options =>
              {
  // ...
                  options.ClaimActions.Remove("amr");
                  options.ClaimActions.DeleteClaim("sid");
                  options.ClaimActions.DeleteClaim("idp");
              });
        }
در اینجا فراخوانی متد Remove، به معنای حذف فیلتری است که کار حذف کردن خودکار claim ویژه‌ی amr را که بیانگر نوع authentication است، انجام می‌دهد (متد Remove در اینجا یعنی مطمئن شویم که amr در لیست claims باقی می‌ماند). همچنین فراخوانی متد DeleteClaim، به معنای حذف کامل یک claim موجود است.

اکنون اگر پس از logout و login، لیست this.User.Claims را بررسی کنیم، دیگر خبری از sid و idp در آن نیست. همچنین claim از نوع amr نیز به صورت پیش‌فرض حذف نشده‌است:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N

افزودن Claim جدید آدرس کاربر

برای افزودن Claim جدید آدرس کاربر، به کلاس src\IDP\DNT.IDP\Config.cs مراجعه کرده و آن‌را به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        // identity-related resources (scopes)
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Address()
            };
        }
در اینجا در لیست Resources، گزینه‌ی IdentityResources.Address نیز اضافه شده‌است که به Claim آدرس مرتبط است.
همین مورد را به لیست AllowedScopes متد GetClients نیز اضافه می‌کنیم:
AllowedScopes =
{
    IdentityServerConstants.StandardScopes.OpenId,
    IdentityServerConstants.StandardScopes.Profile,
    IdentityServerConstants.StandardScopes.Address
},
در آخر Claim متناظر با Address را نیز به اطلاعات هر کاربر، در متد GetUsers، اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
// ...
                    Claims = new List<Claim>
                    {
// ...
                        new Claim("address", "Main Road 1")
                    }
                },
                new TestUser
                {
// ...
                    Claims = new List<Claim>
                    {
// ...
                        new Claim("address", "Big Street 2")
                    }
                }
            };
        }
تا اینجا تنظیمات IDP برای افزودن Claim جدید آدرس به پایان می‌رسد.
پس از آن به کلاس ImageGallery.MvcClient.WebApp\Startup.cs مراجعه می‌کنیم تا درخواست این claim را به لیست scopes میان‌افزار oidc اضافه کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
// ...
              .AddOpenIdConnect("oidc", options =>
              {
   // ...
                  options.Scope.Add("address");
   // …
   options.ClaimActions.DeleteClaim("address");
              });
        }
همچنین می‌خواهیم مطمئن شویم که این claim درون claims identity قرار نمی‌گیرد. به همین جهت DeleteClaim در اینجا فراخوانی شده‌است تا این Claim به کوکی نهایی اضافه نشود تا بتوانیم همواره آخرین مقدار به روز شده‌ی آن‌را از UserInfo Endpoint دریافت کنیم.

یک نکته: فراخوانی DeleteClaim بر روی address غیر ضروری است و می‌شود این سطر را حذف کرد. از این جهت که اگر به سورس OpenID Connect Options مایکروسافت مراجعه کنیم، مشاهده خواهیم کرد که میان‌افزار اعتبارسنجی استاندارد ASP.NET Core، تنها تعداد معدودی از claims را نگاشت می‌کند. به این معنا که هر claim ای که در token وجود داشته باشد، اما اینجا نگاشت نشده باشد، در claims نهایی حضور نخواهند داشت و address claim یکی از این‌ها نیست. بنابراین در لیست نهایی this.User.Claims حضور نخواهد داشت؛ مگر اینکه مطابق همین سورس، با استفاده از متد options.ClaimActions.MapUniqueJsonKey، یک نگاشت جدید را برای آن تهیه کنیم و البته چون نمی‌خواهیم آدرس در لیست claims وجود داشته باشد، این نگاشت را تعریف نخواهیم کرد.


دریافت اطلاعات بیشتری از کاربران از طریق UserInfo Endpoint

همانطور که در قسمت قبل با بررسی «تنظیمات بازگشت Claims کاربر به برنامه‌ی کلاینت» عنوان شد، میان‌افزار oidc با UserInfo Endpoint کار می‌کند که تمام عملیات آن خودکار است. در اینجا امکان کار با آن از طریق برنامه نویسی مستقیم نیز جهت دریافت اطلاعات بیشتری از کاربران، وجود دارد. برای مثال شاید به دلایل امنیتی نخواهیم آدرس کاربر را در لیست Claims او قرار دهیم. این مورد سبب کوچک‌تر شدن کوکی متناظر با این اطلاعات و همچنین دسترسی به اطلاعات به روزتری از کاربر می‌شود.
درخواستی که به سمت UserInfo Endpoint ارسال می‌شود، باید یک چنین فرمتی را داشته باشد:
GET idphostaddress/connect/userinfo
Authorization: Bearer R9aty5OPlk
یک درخواست از نوع GET و یا POST است که به سمت آدرس UserInfo Endpoint ارسال می‌شود. در اینجا ذکر Access token نیز ضروری است. از این جهت که بر اساس scopes ذکر شده‌ی در آن، لیست claims درخواستی مشخص شده و بازگشت داده می‌شوند.

اکنون برای دریافت دستی اطلاعات آدرس از IDP و UserInfo Endpoint آن، ابتدا نیاز است بسته‌ی نیوگت IdentityModel را به پروژه‌ی Mvc Client اضافه کنیم:
dotnet add package IdentityModel
توسط این بسته می‌توان به DiscoveryClient دسترسی یافت و به کمک آن آدرس UserInfo Endpoint را استخراج می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task<IActionResult> OrderFrame()
        {
            var discoveryClient = new DiscoveryClient(_configuration["IDPBaseAddress"]);
            var metaDataResponse = await discoveryClient.GetAsync();

            var userInfoClient = new UserInfoClient(metaDataResponse.UserInfoEndpoint);

            var accessToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            var response = await userInfoClient.GetAsync(accessToken);
            if (response.IsError)
            {
                throw new Exception("Problem accessing the UserInfo endpoint.", response.Exception);
            }

            var address = response.Claims.FirstOrDefault(c => c.Type == "address")?.Value;
            return View(new OrderFrameViewModel(address));
        }
در اینجا یک اکشن متد جدید را جهت سفارش نگارش قاب شده‌ی تصاویر گالری اضافه کرده‌ایم. کار این اکشن متد، دریافت آخرین آدرس شخص از IDP و سپس ارسال آن به View متناظر است. برای این منظور ابتدا یک DiscoveryClient را بر اساس آدرس IDP تشکیل داده‌ایم. حاصل آن متادیتای این IDP است که یکی از مشخصات آن UserInfoEndpoint می‌باشد. سپس بر این اساس، UserInfoClient را تشکیل داده و Access token جاری را به سمت این endpoint ارسال می‌کنیم. حاصل آن، آخرین Claims کاربر است که مستقیما از IDP دریافت شده و از کوکی جاری کاربر خوانده نمی‌شود. سپس از این لیست، مقدار Claim متناظر با address را استخراج کرده و آن‌را به View ارسال می‌کنیم.
OrderFrameViewModel ارسالی به View، یک چنین شکلی را دارد:
namespace ImageGallery.MvcClient.ViewModels
{
    public class OrderFrameViewModel
    {
        public string Address { get; } = string.Empty;

        public OrderFrameViewModel(string address)
        {
            Address = address;
        }
    }
}
و View متناظر با این اکشن متد در آدرس Views\Gallery\OrderFrame.cshtml به صورت زیر تعریف شده‌است که آدرس شخص را نمایش می‌دهد:
@model ImageGallery.MvcClient.ViewModels.OrderFrameViewModel
<div class="container">
    <div class="h3 bottomMarginDefault">Order a framed version of your favorite picture.</div>
    <div class="text bottomMarginSmall">We've got this address on record for you:</div>
    <div class="text text-info bottomMarginSmall">@Model.Address</div>
    <div class="text">If this isn't correct, please contact us.</div>
</div>

سپس به Shared\_Layout.cshtml مراجعه کرده و لینکی را به این اکشن متد و View، اضافه می‌کنیم:
<li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>

اکنون اگر برنامه را اجرا کنیم، پس از login، یک چنین خروجی قابل مشاهده است:


همانطور که ملاحظه می‌کنید، آدرس شخص به صورت مستقیم از UserInfo Endpoint دریافت و نمایش داده شده‌است.


بررسی Authorization مبتنی بر نقش‌ها

تا اینجا مرحله‌ی Authentication را که مشخص می‌کند کاربر وار شده‌ی به سیستم کیست، بررسی کردیم که اطلاعات آن از Identity token دریافتی از IDP استخراج می‌شود. مرحله‌ی پس از ورود به سیستم، مشخص کردن سطوح دسترسی کاربر و تعیین این مورد است که کاربر مجاز به انجام چه کارهایی می‌باشد. به این مرحله Authorization می‌گویند و روش‌های مختلفی برای مدیریت آن وجود دارد:
الف) RBAC و یا Role-based Authorization و یا تعیین سطوح دسترسی بر اساس نقش‌های کاربر
در این حالت claim ویژه‌ی role، از IDP دریافت شده و توسط آن یکسری سطوح دسترسی کاربر مشخص می‌شوند. برای مثال کاربر وارد شده‌ی به سیستم می‌تواند تصویری را اضافه کند و یا آیا مجاز است نگارش قاب شده‌ی تصویری را درخواست دهد؟
ب) ABAC و یا Attribute based access control روش دیگر مدیریت سطوح دسترسی است و عموما آن‌را نسبت به حالت الف ترجیح می‌دهند که آن‌را در قسمت‌های بعدی بررسی خواهیم کرد.

در اینجا روش «تعیین سطوح دسترسی بر اساس نقش‌های کاربر» را بررسی می‌کنیم. برای این منظور به تنظیمات IDP در فایل src\IDP\DNT.IDP\Config.cs مراجعه کرده و claims جدیدی را تعریف می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static List<TestUser> GetUsers()
        {
            return new List<TestUser>
            {
                new TestUser
                {
//...
                    Claims = new List<Claim>
                    {
//...
                        new Claim("role", "PayingUser")
                    }
                },
                new TestUser
                {
//...
                    Claims = new List<Claim>
                    {
//...
                        new Claim("role", "FreeUser")
                    }
                }
            };
        }
در اینجا دو نقش FreeUser و PayingUser به صورت claimهایی جدید به دو کاربر IDP اضافه شده‌اند.

سپس باید برای این claim جدید یک scope جدید را نیز به قسمت GetIdentityResources اضافه کنیم تا توسط client قابل دریافت شود:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<IdentityResource> GetIdentityResources()
        {
            return new List<IdentityResource>
            {
                // ...
                new IdentityResource(
                    name: "roles",
                    displayName: "Your role(s)",
                    claimTypes: new List<string>() { "role" })
            };
        }
چون roles یکی از scopeهای استاندارد نیست، آن‌را به صورت یک IdentityResource سفارشی تعریف کرده‌ایم. زمانیکه scope ای به نام roles درخواست می‌شود، لیستی از نوع claimTypes بازگشت داده خواهد شد.

همچنین باید به کلاینت مجوز درخواست این scope را نیز بدهیم. به همین جهت آن‌را به AllowedScopes مشخصات Client نیز اضافه می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
// ...
                    AllowedScopes =
                    {
// ...
                        "roles"
                    }
// ...
                }
             };
        }

در ادامه قرار است تنها کاربری که دارای نقش PayingUser است،  امکان دسترسی به سفارش نگارش قاب شده‌ی تصاویر را داشته باشد. به همین جهت به کلاس ImageGallery.MvcClient.WebApp\Startup.cs برنامه‌ی کلاینت مراجعه کرده و درخواست scope نقش‌های کاربر را به متد تنظیمات AddOpenIdConnect اضافه می‌کنیم:
options.Scope.Add("roles");

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


اما اگر به لیست موجود در this.User.Claims در برنامه‌ی کلاینت مراجعه کنیم، نقش او را مشاهده نخواهیم کرد و به این لیست اضافه نشده‌است:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N

همانطور که در نکته‌ای پیشتر نیز ذکر شد، چون role جزو لیست نگاشت‌های OpenID Connect Options مایکروسافت نیست، آن‌را به صورت خودکار به لیست claims اضافه نمی‌کند؛ دقیقا مانند آدرسی که بررسی کردیم. برای رفع این مشکل و افزودن نگاشت آن در متد تنظیمات AddOpenIdConnect، می‌توان از متد MapUniqueJsonKey به صورت زیر استفاده کرد:
options.ClaimActions.MapUniqueJsonKey(claimType: "role", jsonKey: "role");
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا به آن وارد شوید. اینبار لیست this.User.Claims به صورت زیر تغییر کرده‌است که شامل role نیز می‌باشد:
• Claim type: sub - Claim value: d860efca-22d9-47fd-8249-791ba61b07c7
• Claim type: amr - Claim value: pwd
• Claim type: given_name - Claim value: Vahid
• Claim type: family_name - Claim value: N
• Claim type: role - Claim value: PayingUser


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

در ادامه قصد داریم لینک درخواست تصاویر قاب شده را فقط برای کاربرانی که دارای نقش PayingUser هستند، نمایش دهیم. به همین جهت به فایل Views\Shared\_Layout.cshtml مراجعه کرده و آن‌را به صورت زیر تغییر می‌دهیم:
@if(User.IsInRole("PayingUser"))
{
   <li><a asp-area="" asp-controller="Gallery" asp-action="OrderFrame">Order a framed picture</a></li>
}
در این حالت از متد استاندارد User.IsInRole برای بررسی نقش کاربر جاری استفاده شده‌است. اما این متد هنوز نمی‌داند که نقش‌ها را باید از کجا دریافت کند و اگر آن‌را آزمایش کنید، کار نمی‌کند. برای تکمیل آن به فایل ImageGallery.MvcClient.WebApp\Startup.cs مراجعه کرده تنظیمات متد AddOpenIdConnect را به صورت زیر تغییر می‌دهیم:
options.TokenValidationParameters = new TokenValidationParameters
{
    NameClaimType = JwtClaimTypes.GivenName,
    RoleClaimType = JwtClaimTypes.Role,
};
TokenValidationParameters نحوه‌ی اعتبارسنجی توکن دریافتی از IDP را مشخص می‌کند. همچنین مشخص می‌کند که چه نوع claim ای از token دریافتی  از IDP به RoleClaimType سمت ASP.NET Core نگاشت شود.
اکنون برای آزمایش آن یکبار از سیستم خارج شده و مجددا به آن وارد شوید. پس از آن لینک درخواست نگارش قاب شده‌ی تصاویر، برای کاربر User 1 نمایان خواهد بود و نه برای User 2 که FreeUser است.

البته هرچند تا این لحظه لینک نمایش View متناظر با اکشن متد OrderFrame را امن کرده‌ایم، اما هنوز خود این اکشن متد به صورت مستقیم با وارد کردن آدرس https://localhost:5001/Gallery/OrderFrame در مرورگر قابل دسترسی است. برای رفع این مشکل به کنترلر گالری مراجعه کرده و دسترسی به اکشن متد OrderFrame را توسط فیلتر Authorize و با مقدار دهی خاصیت Roles آن محدود می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        [Authorize(Roles = "PayingUser")]
        public async Task<IActionResult> OrderFrame()
        {
خاصیت Roles فیلتر Authorize، می‌تواند چندین نقش جدا شده‌ی توسط کاما را نیز دریافت کند.
برای آزمایش آن، توسط مشخصات کاربر User 2 به سیستم وارد شده و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. در این حالت یک چنین تصویری نمایان خواهد شد:


همانطور که مشاهده می‌کنید، علاوه بر عدم دسترسی به این اکشن متد، به صفحه‌ی Account/AccessDenied که هنوز در برنامه‌ی کلاینت تعریف نشده‌است، هدایت شده‌ایم. به همین جهت خطای 404 و یا یافت نشد، نمایش داده شده‌است.
برای تغییر مقدار پیش‌فرض صفحه‌ی عدم دسترسی، ابتدا Controllers\AuthorizationController.cs را با این محتوا ایجاد می‌کنیم:
using Microsoft.AspNetCore.Mvc;

public class AuthorizationController : Controller
{
    public IActionResult AccessDenied()
    {
        return View();
    }
}
سپس View آن‌را در فایل Views\Authorization\AccessDenied.cshtml به صورت زیر تکمیل خواهیم کرد که لینک به logout نیز در آن وجود دارد:
<div class="container">
    <div class="h3">Woops, looks like you're not authorized to view this page.</div>
    <div>Would you prefer to <a asp-controller="Gallery" asp-action="Logout">log in as someone else</a>?</div>
</div>

اکنون نیاز است تا این آدرس جدید را به کلاس ImageGallery.MvcClient.WebApp\Startup.cs معرفی کنیم.
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                // ...
            }).AddCookie("Cookies", options =>
              {
                  options.AccessDeniedPath = "/Authorization/AccessDenied";
              })
// ...
آدرس جدید Authorization/AccessDenied باید به تنظیمات AddCookie اضافه شود تا توسط سیستم شناخته شده و مورد استفاده قرار گیرد. علت اینجا است که role claims، از کوکی رمزنگاری شده‌ی برنامه استخراج می‌شوند. به همین جهت تنظیم آن مرتبط است به AddCookie.
برای آزمایش آن، یکبار از برنامه خارج شده و مجددا با اکانت User 2 به آن وارد شوید و آدرس https://localhost:5001/Gallery/OrderFrame را مستقیما در مرورگر وارد کنید. اینبار تصویر زیر که همان آدرس جدید تنظیم شده‌است نمایش داده خواهد شد:




کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
نظرات مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
در انجمن تلریک   چنین بحثی شده بود و پاسخی که داده شده بود به شکل زیر است:
<div id="imgBrowser"></div>

$("#imgBrowser").kendoImageBrowser({
    transport: {
        read: "/service/ImageBrowser/Read",
        destroy: {
            url: "/service/ImageBrowser/Destroy",
            type: "POST"
        },
        create: {
            url: "/service/ImageBrowser/Create",
            type: "POST"
        },
        thumbnailUrl: "/service/ImageBrowser/Thumbnail",
        uploadUrl: "/service/ImageBrowser/Upload",
        imageUrl: "/service/ImageBrowser/Image?path={0}"
    }
});
بنده خودم هم تست کردم . 

مطالب
نصب Mono 3.0 بر روی Ubuntu
با استفاده از مونو امکان اجرای برنامه‌های دات نت تحت لینوکس وجود دارند. در ادامه سعی خواهیم کرد تا نگارش 3 آن‌را بر روی اوبونتو نصب کنیم. مونو 3 تا دات نت 4 و نیم را پشتیبانی می‌کند.

دریافت اوبونتو
برای دریافت اوبونتو به آدرس ذیل مراجعه نمائید.
نسخه سرور آن GUI ندارد (هرچند بعدا در طی یک بسته 450 مگابایتی قابل نصب است). نسخه دسکتاپ آن به همراه GUI نیز هست. البته برای نصب دات نت بر روی آن این مساله تفاوتی نمی‌کند. برای نصب آزمایشی و مجازی آن هم می‌توانید برای مثال از VMWare workstation استفاده کنید؛ بدون اینکه نیاز داشته باشید این توزیع خاص لینوکس را واقعا بر روی کامپیوتر خود نصب کنید.

در تمام قسمت‌های ذیل فرض بر این است که ترمینال خط فرمان لینوکس را گشوده‌اید و همچنین سیستم به اینترنت وصل است.



دریافت Git و Curl

ابتدا دستور زیر را در خط فرمان لینوکس اجرا کنید تا سیستم بسته‌های لینوکس به روز شده و همچنین یک سری پیشنیاز مانند git ، curl و امثال آن نصب شوند (کتابخانه curl جهت استفاده در محیط‌های برنامه نویسی کاربرد دارد و همچنین برنامه پیشرفته‌ای است برای کار با وب و دریافت فایل‌ها):


 sudo apt-get update && sudo apt-get -y install git-core curl python-software-properties
sudo apt-get install build-essential automake checkinstall intltool git

نصب آخرین نگارش Mono و وابستگی‌های آن

در ادامه نوبت به نصب آخرین نگارش مونو است. از روش متداول ذیل برای نصب مونو استفاده نکنید :
 sudo apt-get install mono-complete
این دستور نگارش 2.10.8.1 را تا این تاریخ بر روی سیستم شما نصب خواهد کرد و اگر پیشتر مونو را به این روش نصب کرده‌اید، با استفاده از دستور ذیل آن‌را حذف کنید:
 sudo apt-get purge mono-runtime
برای دسترسی به آخرین نگارش نگارش مونو، نیاز است آن‌را از روی سورس آن کامپایل کنیم. اسکریپت کامل نصب آن‌را در آدرس ذیل می‌توانید پیدا کنید:
و یا اگر آدرس فوق برقرار نبود از اینجا: install_mono-3.0-sh
برای نمونه جهت نصب mono نگارش 3 از اسکریپت install_mono-3.0.sh به نحو ذیل استفاده خواهیم کرد (این دستورات را به ترتیب در ترمینال لینوکس اجرا کنید):
 mkdir mono-3.0
cd mono-3.0
wget --no-check-certificate https://github.com/nathanb/iws-snippets/raw/master/mono-install-scripts/ubuntu/install_mono-3.0.sh
chmod 755 install_mono-3.0.sh
./install_mono-3.0.sh
این پروسه مدتی طول خواهد کشید (تا تمام بسته‌های لازم از اینترنت دریافت شوند). استفاده از اسکریپت فوق کار را بسیار ساده کرده و بسیاری از مراحل لازم نصب مونو را یکجا در خود به همراه دارد. مونو 3 تا دات نت 4 و نیم را پشتیبانی می‌کند.


بعد از اجرای فرمان فوق به خطای ذیل خواهید رسید:
 config.status: error: cannot find input file: `po/mcs/Makefile.in.in'
این مورد مشکلی است که در نگارش 3.0.10 رخ داده و فراموش کرده‌اند که یک پوشه را کپی کنند (در نگارش‌های قبلی آن این مشکل وجود نداشته و با توجه به آگاه شدن از آن، در نگارش‌های بعدی نیز نباید مشکلی باشد).
برای رفع آن ابتدا به مسیر ذیل وارد شوید (پوشه build/mono-3.0.10/po)، فایل mcs را حذف (این مورد در اصل یک پوشه است و نه یک فایل) و سپس بسته اصلی mono را از github دریافت کنید. آن‌را unzip کرده و کل پوشه mcs داخل آن‌را به درون پوشه po جاری کپی کنید. سپس فایل zip دریافت شده را حذف کنید:
 cd mono-3.0/build/mono-3.0.10/po
rm mcs
wget https://github.com/mono/mono/archive/master.zip
unzip master.zip
mv mcs/ mono-3.0/build/mono-3.0.10/po
rm -rf mono-master master.zip
البته برای اینکه وقت شما زیاد تلف نشود، پوشه mcs نگارش 3.0.10 را از آدرس ذیل دریافت و پس از unzip درون پوشه mono-3.0/build/mono-3.0.10/po کپی کنید. (6 سطر فوق هم نیازی به اجرا ندارند)

پس از باز سازی پوشه مفقود mcs، باید مرحله «building mono packages» موجود در فایل install_mono-3.0.sh اجرا شود. برای این منظور، فایل final-build-mono-3.0.sh را  از آدرس ذیل دریافت و در کنار فایل install_mono-3.0.sh موجود کپی کنید.


سپس در خواص این فایل، مجوز execute را نیز فعال نمائید. اکنون آن‌را اجرا کنید:
 ./final-build-mono-3.0.sh
فایل final-build-mono-3.0.sh در حقیقت همان فایل install_mono-3.0.sh اصلی است. با این تفاوت که قسمت ابتدای فایل که در آن وابستگی‌های لازم از اینترنت دریافت می‌شدند، حذف شده است. چون پیشتر اینکار را انجام داده بودیم (با اجرای اولیه آن).


اکنون مدتی صبر کنید تا کار کامپایل نهایی تمام بسته‌های دریافت شده پس از اجرای اسکریپت final-build-mono-3.0.sh انجام شود.


آزمایش Mono نصب شده

برای اینکه مطمئن شویم، Mono درست نصب شده است، دستور زیر را در خط فرمان صادر کنید:
 /opt/mono-3.0/bin/mono -V


برای اینکه این مسیر را به Path لینوکس اضافه کنیم تا قادر شویم فرمان mono را در هر مسیری اجرا کنیم، ابتدا دستور ذیل را اجرا کرده
 sudo nano /etc/environment
و سپس در ادیتور باز شده، مسیر و عبارات ذیل را به انتهای مقدار جاری اضافه کنید:
 :/opt/mono-3.0/bin


بعد ctrl+x را زده، به پیام ذخیره سازی تغییرات پاسخ مثبت دهید. سپس نیاز است یکبار logoff و login کنید تا این تغییرات اعمال شوند.


یک نکته تکمیلی:
اگر به صفحه نگارش‌های رسمی Mono 3.x مراجعه کنید، نگارش‌های جدیدتری را نیز می‌توانید ملاحظه کنید. فایل‌های قابل نصب آن‌ها نیر در آدرس‌های ذیل قرار دارند:
برای استفاده از اسکریپت install_mono-3.0.sh با این نگارش‌های جدیدتر فقط کافی است تعاریف ذیل را بر اساس شماره نگارش بسته‌های جدید اصلاح کنید:
PACKAGES=("mono-3.0.10"
"libgdiplus-2.10.9"
"gtk-sharp-2.12.11"
"xsp-2.10.2"
"mod_mono-2.10")

URLS=("http://download.mono-project.com/sources/mono/mono-3.0.10.tar.bz2"
"http://download.mono-project.com/sources/libgdiplus/libgdiplus-2.10.9.tar.bz2"
"http://download.mono-project.com/sources/gtk-sharp212/gtk-sharp-2.12.11.tar.bz2"
"http://download.mono-project.com/sources/xsp/xsp-2.10.2.tar.bz2"
"http://download.mono-project.com/sources/mod_mono/mod_mono-2.10.tar.bz2")
نظرات مطالب
ASP.NET MVC #10
با سلام
آیا mvc در binding نوع داده decimal مشکلی دارد؟
من یک مدل مانند زیر ساخته ام
public class TestDecimal
    {
        public string TestName { get; set; }
        public int TestInt { get; set; }
        public decimal TestDecimal1 { get; set; }
        public decimal TestDecimal2 { get; set; }
        public decimal? TestDecimal3 { get; set; }

    }
حالا کنترلر رو کامل میکنم
public ActionResult test()
        {
            var model = new TestDecimal();
            return View(model);
        }

        [HttpPost]
        public ActionResult test(TestDecimal model)
        {
            return View(model);
        }
و در آخر view
@model Test.Models.TestDecimal

<h2>test decimal</h2>
@using (Ajax.BeginForm(
    actionName: "test",
    controllerName: "DocRate",
    ajaxOptions: new AjaxOptions
    {
        HttpMethod = "POST",
        InsertionMode = InsertionMode.Replace
    }))
{
    <div dir="ltr">
        @Html.TextBoxFor(m => m.TestName)
        @Html.TextBoxFor(m => m.TestInt)
        @Html.TextBoxFor(m => m.TestDecimal1)
        @Html.TextBoxFor(m => m.TestDecimal2)
        @Html.TextBoxFor(m => m.TestDecimal3)

        <br/>
        <input id="submitRate" type="submit" value=" ثبت امتیاز"/>
    </div>
}
نتیجه نهایی

 جالبه؟ با اینکه فیلدهای decimal پر شده ولی نتیجه bind نمیشه
همین فیلدهای decimal رو اگر با اعداد صحیح نه اعشاری پر کنم binding انجام میشود!
مشکل از کجا است؟
مطالب
عمومی سازی الگوریتم‌ها با استفاده از Reflection
در این مقاله قصد داریم عملیات Reflection را بیشتر در انجام ساده‌تر عملیات ببینیم. عملیاتی که به همراه کار اضافه، تکراری و خسته کننده است و با استفاده از Reflection این کارها حذف شده و تعداد خطوط هم پایین می‌آید. حتی گاها ممکن است موجب استفاده‌ی مجدد از کدها شود که همگی این عوامل موجب بالا رفتن امتیاز Refactoring می‌شوند.
در مثال‌های زیر مجموعه‌ای از Reflection‌های ساده و کاملا کاربردی است که من با آن‌ها روبرو شده ام.


کوتاه سازی کدهای نمایش یک View در ASP.NET MVC با Reflection 

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

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

ساختار اطلاعاتی تصویر فوق به شرح زیر است:
 <div>
                              <div>
                                  <div>
                                      <p><span>First Name </span>: Jonathan</p>
                                  </div>
   </div>
                          </div>
که به دو فایل پارشال تقسیم شده است Bio_ و BioRow_ که محتویات هر پارشال هم به شرح زیر است:

BioRow_
@model System.Web.UI.WebControls.ListItem


<div>
    <p><span>@Model.Text </span>: @Model.Value</p>
</div>
در پارشال بالا ورودی از نوع listItem است که یک متن دارد و یک مقدار.(شاید به نظر شبیه حالت جفت کلید و مقدار باشد ولی در این کلاس خبری از کلید نیست).
پارشال پایینی هم دربرگیرنده‌ی پارشال بالاست که قرار است چندین و چند بار پارشال بالا در خودش نمایش دهد.
Bio_
@using System.Web.UI.WebControls
@using ZekrWepApp.Filters
@model ZekrModel.Admin

    <div>
        <h1>Bio Graph</h1>
        <div>
            
            @{
                ListItemCollection collection = GetCustomProperties.Get(Model,exclude:new string[]{"Poems","Id"});
                foreach (var item in collection)
                {
                    Html.RenderPartial(MVC.Shared.Views._BioRow, item);
                }
            }

                    </div>
    </div>
پارشال بالا یک مدل از کلاس Admin را می‌پذیرد که قرار است اطلاعات شخصی مدیر را نمایش دهد. در ابتدا متدی از یک کلاس ایستا وجود دارد که کدهای Reflection درون آن قرار دارند که یک مجموعه از ListItem‌ها را بر می‌گرداند و سپس با یک حلقه، پارشال BioRow_ را صدا می‌زند.

کد درون این کلاس ایستا را بررسی می‌کنیم؛ این کلاس دو متد دارد یکی عمومی و دیگری خصوصی است:
  public class GetCustomProperties
    {
        private static PropertyInfo[] getObjectsInfos(object obj,string[] inclue,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (inclue != null)
            {           
                return list.Where(propertyInfo => inclue.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }
    }
کد بالا که یک کد خصوصی است، سه پارامتر را می‌پذیرد. اولی مدل یا کلاسی است که به آن پاس کرده‌ایم. دو پارامتر بعدی اختیاری است و در کد پارشال بالا Exclude را تعریف کرده ایم و تنهای یکی از دو پارامتر بالا هم باید مورد استفاده قرار بگیرند و Include ارجحیت دارد. وظیفه‌ی این دو پارمتر این است که آرایه ای از رشته‌ها را دریافت می‌کنند که نام پراپرتی‌ها در آن‌ها ذکر شده است. آرایه Include می‌گوید که فقط این پراپرتی‌ها را برگردان ولی اگر دوست دارید همه‌ی پارامترها را نمایش دهید و تنها یکی یا چندتا از آن‌ها را حذف کنید، از آرایه Exclude استفاده کنید. در صورتی که این دو آرایه خالی باشند، همه‌ی پراپرتی‌ها بازگشت داده می‌شوند و در صورتی که یکی از آن‌ها وارد شده باشد، طبق دستورات Linq بالا بررسی می‌کند که (Include) آیا اسامی مشترکی بین آن‌ها وجود دارد یا خیر؟ اگر وجود دارد آن را در لیست قرار داده و بر می‌گرداند و در حالت Exclude این مقایسه به صورت برعکس انجام می‌گیرد و باید لیستی برگردانده شود که اسامی، نکته مشترکی نداشته باشند.

متد عمومی که در این کلاس قرار دارد به شرح زیر است:
 public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {


                string name = propertyInfo.Name;

                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
این متد سه پارامتر را از کاربر دریافت و به سمت متد خصوصی ارسال می‌کند. موقعی‌که پراپرتی‌ها بازگشت داده می‌شوند، دو قسمت آن مهم است؛ یکی عنوان پراپرتی و دیگری مقدار پراپرتی. از آن جا که نام پراپرتی‌ها طبق سلیقه‌ی برنامه نویس و با حروف انگلیسی نوشته می‌شوند، در صورتی که برنامه نویس از متادیتای Display در مدل بهره برده باشد، به جای نام پراپرتی مقداری را که به متادیتای Display داده‌ایم، بر می‌گردانیم.

کد بالا پراپرتی‌ها را دریافت و یک به یک متادیتاهای آن را بررسی کرده و در صورتی که از متادیتای Display استفاده کرده باشند، مقدار آن را جایگزین نام پراپرتی خواهد کرد. در مورد مقدار هم از آنجا که اگر پراپرتی با Null پر شده باشد، تبدیل به رشته‌ای با پیام خطای روبرو خواهد شد. در نتیجه بهتر است یک شرط احتیاط هم روی آن پیاده شود. در آخر هم از متن و مقدار، یک آیتم ساخته و درون Collection اضافه می‌کنیم و بعد از اینکه همه پراپرتی‌ها بررسی شدند، Collection را بر می‌گردانیم.
 [Display(Name = "نام کاربری")]
public string UserName { get; set; }

کد کامل کلاس:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web;
using System.Web.Mvc.Html;
using System.Web.UI.WebControls;
using Links;

namespace ZekrWepApp.Filters
{
    
    public class GetCustomProperties
    {
        public static ListItemCollection Get(object obj,string[] inclue=null,string[] exclude=null)
        {
            var propertyInfos = getObjectsInfos(obj, inclue, exclude);
            if (propertyInfos == null) throw new ArgumentNullException("propertyInfos is null");

            var collection = new ListItemCollection();

            foreach (PropertyInfo propertyInfo in propertyInfos)
            {
                string name = propertyInfo.Name;
                foreach (Attribute attribute in propertyInfo.GetCustomAttributes(true))
                {
                    DisplayAttribute displayAttribute = attribute as DisplayAttribute;

                    if (displayAttribute != null)
                    {
                        name = displayAttribute.Name;
                        break;
                    }                  
                }

                string value = "";
                object objvalue = propertyInfo.GetValue(obj);
                if (objvalue != null) value = objvalue.ToString();

                collection.Add(new ListItem(name,value));
            }
            return collection;
        }
        private static PropertyInfo[] getObjectsInfos(object obj,string[] include,string[] exclude )
        {
            var list = obj.GetType().GetProperties();

            PropertyInfo[] outputPropertyInfos = null;

            if (include != null)
            {           
                return list.Where(propertyInfo => include.Contains(propertyInfo.Name)).ToArray();
            }
            if (exclude != null)
            {
                return list.Where(propertyInfo => !exclude.Contains(propertyInfo.Name)).ToArray();
            }
            return list;
        }  
    }   
}

لیستی از پارامترها با Reflection


مورد بعدی که ساده‌تر بوده و از کد بالا مختصرتر هم هست، این است که قرار بود برای یک درگاه، یک سری اطلاعات را با متد Post ارسال کنم که نحوه‌ی ارسال اطلاعات به شکل زیر بود:
amount=1000&orderId=452&Pid=xxx&....
کد زیر را من جهت ساخت قالب‌های این چنینی استفاده می‌کنم:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Utils
{
    public class QueryStringParametersList
    {
        private string Symbol = "&";
        private List<KeyValuePair<string, string>> list { get; set; }

        public QueryStringParametersList()
        {
            list = new List<KeyValuePair<string, string>>();
        }
        public QueryStringParametersList(string symbol)
        {
            Symbol = symbol;
           list = new List<KeyValuePair<string, string>>(); 
        }

        public int Size
        {
            get { return list.Count; }
        }
        public void Add(string key, string value)
        {
            list.Add(new KeyValuePair<string, string>(key, value));
        }

        public string GetQueryStringPostfix()
        {
            return string.Join(Symbol, list.Select(p => Uri.EscapeDataString(p.Key) + "=" + Uri.EscapeDataString(p.Value)));
        }
    }
}



یک متغیر به نام symbol دارد و در صورتی در شرایط متفاوت، قصد چسپاندن چیزی را به یکدیگر با علامتی خاص داشته باشید، این تابع می‌تواند کاربرد داشته باشد. این متد از یک لیست کلید و مقدار استفاده کرده و پارامترهایی را که به آن پاس می‌شود، نگهداری و سپس توسط متد GetQueryStringPostfix آن‌ها را با یکدیگر الحاق کرده و در قالب یک رشته بر می‌گرداند.
کاربرد Reflection در اینجا این است که من باید دوبار به شکل زیر، دو نوع اطلاعات متفاوت را پست کنم. یکی موقع ارسال به درگاه و دیگری موقع بازگشت از درگاه.
QueryStringParametersList queryparamsList = new QueryStringParametersList();

            ueryparamsList.Add("consumer_key", requestPayment.Consumer_Key);
            queryparamsList.Add("amount", requestPayment.Amount.ToString());
            queryparamsList.Add("callback", requestPayment.Callback);
            queryparamsList.Add("description", requestPayment.Description);
            queryparamsList.Add("email", requestPayment.Email);
            queryparamsList.Add("mobile", requestPayment.Mobile);
            queryparamsList.Add("name", requestPayment.Name);
            queryparamsList.Add("irderid", requestPayment.OrderId.ToString());

ولی با استفاده از کد Reflection که در بالاتر عنوان شد، باید نام و مقدار پراپرتی را گرفته و در یک حلقه آن‌ها را اضافه کنیم، بدین شکل:
   private QueryStringParametersList ReadParams(object obj)
        {
            PropertyInfo[] propertyInfos = obj.GetType().GetProperties();

            QueryStringParametersList queryparamsList = new QueryStringParametersList();
            for (int i = 0; i < propertyInfos.Count(); i++)
            {
                queryparamsList.Add(propertyInfos[i].Name.ToLower(),propertyInfos[i].GetValue(obj).ToString() );              
            }
            return queryparamsList;
        }
در کد بالا هر بار پراپرتی‌های کلاس را خوانده و نام و مقدار آن‌ها را گرفته و به کلاس QueryString اضافه می‌کنیم. پارامتر ورودی این متد به این خاطر object در نظر گرفته شده است که تا هر کلاسی را بتوانیم به آن پاس کنیم که خودم در همین کلاس درگاه، دو کلاس را به آن پاس کردم.
مطالب
نوشتن افزونه برای مرورگرها: قسمت دوم : کروم
در مقاله پیشین ما ظاهر افزونه را طراحی و یک سری از قابلیت‌های افزونه را معرفی کردیم. در این قسمت قصد داریم پردازش پس زمینه افزونه یعنی خواندن RSS و اعلام به روز آوری سایت را مورد بررسی قرار دهیم و یک سری قابلیت هایی که گوگل در اختیار ما قرار داده است.

خواندن RSS توسط APIهای گوگل
گوگل در تعدادی از زمینه‌ها و سرویس‌های خودش apiهایی را ارائه کرده است که یکی از آن ها خواندن فید است و ما از آن برای خواندن RSS یا اتم وب سایت کمک می‌گیریم. روند کار بدین صورت است که ابتدا ما بررسی می‌کنیم کاربر چه مقادیری را ثبت کرده است و افزونه قرار است چه بخش هایی از وب سایت را بررسی نماید. در این حین، صفحه پس زمینه شروع به کار کرده و در هر سیکل زمانی مشخص شده بررسی می‌کند که آخرین بار چه زمانی RSS به روز شده است. اگر از تاریخ قبلی بزرگتر باشد، پس سایت به روز شده است و تاریخ جدید را برای دفعات آینده جایگزین تاریخ قبلی کرده و یک پیام را به صورت نوتیفیکیشن جهت اعلام به روز رسانی جدید در آن بخش به کاربر نشان می‌دهد.
 اجازه دهید کدها را کمی شکیل‌تر کنیم. من از فایل زیر که یک فایل جاوااسکریپتی است برای نگه داشتن مقادیر بهره می‌برم تا اگر روزی خواستم یکی از آن‌ها را تغییر دهم راحت باشم و در همه جا نیاز به تغییر مجدد نداشته نباشم. نام فایل را (const.js) به خاطر ثابت بودن آن‌ها انتخاب کرده‌ام.
  //برای ذخیره مقادیر از ساختار نام و مقدار استفاده می‌کنیم که نام‌ها را اینجا ثبت کرده ام
var Variables={
 posts:"posts",
 postsComments:"postsComments",
 shares:"shares",
 sharesComments:"sharesComments",
}

//برای ذخیره زمان آخرین تغییر سایت برای هر یک از مطالب به صورت جداگانه نیاز به یک ساختار نام و مقدار است که نام‌ها را در اینجا ذخیره کرده ام
var DateContainer={
 posts:"dtposts",
 postsComments:"dtpostsComments",
 shares:"dtshares",
 sharesComments:"dtsharesComments",
 interval:"interval"
}
 
//برای نمایش پیام‌ها به کاربر
var Messages={
SettingsSaved:"تنظیمات ذخیره شد",
SiteUpdated:"سایت به روز شد",
PostsUpdated:"مطلب ارسالی جدید به سایت اضافه شد",
CommentsUpdated:"نظری جدیدی در مورد مطالب سایت ارسال شد",
SharesUpdated:"اشتراک جدید به سایت ارسال شد",
SharesCommentsUpdated:"نظری برای اشتراک‌های سایت اضافه شد"
}
//لینک‌های فید سایت
var Links={
 postUrl:"https://www.dntips.ir/feeds/posts",
 posts_commentsUrl:"https://www.dntips.ir/feeds/comments",
 sharesUrl:"https://www.dntips.ir/feed/news",
 shares_CommentsUrl:"https://www.dntips.ir/feed/newscomments"
}
//لینک صفحات سایت
var WebLinks={
Home:"https://www.dntips.ir",
 postUrl:"https://www.dntips.ir/postsarchive",
 posts_commentsUrl:"https://www.dntips.ir/commentsarchive",
 sharesUrl:"https://www.dntips.ir/newsarchive",
 shares_CommentsUrl:"https://www.dntips.ir/newsarchive/comments"
}
موقعی که اولین بار افزونه نصب می‌شود، باید مقادیر پیش فرضی وجود داشته باشند که یکی از آن‌ها مربوط به مقدار سیکل زمانی است (هر چند وقت یکبار فید را چک کند) و دیگری ذخیره مقادیر پیش فرض رابط کاربری که قسمت پیشین درست کردیم؛ پروسه پس زمینه برای کار خود به آن‌ها نیاز دارد و بعدی هم تاریخ نصب افزونه است برای اینکه تاریخ آخرین تغییر سایت را با آن مقایسه کند که البته با اولین به روزرسانی تاریخ فید جای آن را می‌گیرد. جهت انجام اینکار یک فایل init.js ایجاد کرده‌ام که قرار است بعد از نصب افزونه، مقادیر پیش فرض بالا را ذخیره کنیم.
chrome.runtime.onInstalled.addListener(function(details) {
var now=String(new Date());

var params={};
params[Variables.posts]=true;
params[Variables.postsComments]=false;
params[Variables.shares]=false;
params[Variables.sharesComments]=false;

params[DateContainer.interval]=1;

params[DateContainer.posts]=now;
params[DateContainer.postsComments]=now;
params[DateContainer.shares]=now;
params[DateContainer.sharesComments]=now;

 chrome.storage.local.set(params, function() {
  if(chrome.runtime.lastError)
   {
       /* error */
       console.log(chrome.runtime.lastError.message);
       return;
   }
        });
});
chrome.runtime شامل رویدادهایی چون onInstalled ، onStartup ، onSuspend و ... است که مربوطه به وضعیت اجرایی افزونه میشود. آنچه ما اضافه کردیم یک listener برای زمانی است که افزونه نصب شده است و در آن مقادیر پیش فرض ذخیره می‌شوند. اگر خوب دقت کنید می‌بینید که روش دخیره سازی ما در اینجا کمی متفاوت از مقاله پیشین هست و شاید پیش خودتان بگویید که احتمالا به دلیل زیباتر شدن کد اینگونه نوشته شده است ولی مهمترین دلیل این نوع نوشتار این است که متغیرهای بین {} آنچنان فرقی با خود string نمی‌کنند یعنی کد زیر:
chrome.storage.local.set('mykey':myvalue,....
با کد زیر برابر است:
chrome.storage.local.set(mykey:myvalue,...
پس اگر مقداری را داخل متغیر بگذاریم آن مقدار حساب نمی‌شود؛ بلکه کلید نام متغیر خواهد شد.
 برای معرفی این دو فایل const.js و init.js به manifest.json می‌توانید به صورت زیر عمل کنید:
"background": {
    "scripts": ["const.js","init.js"]
}
در این حالت خود اکستنشن در زمان نصب یک فایل html درست کرده و این دو فایل js را در آن صدا میزند که البته خود ما هم می‌توانیم اینکار را مستقیما انجام دهیم. مزیت اینکه ما خودمان مسقیما این کار را انجام دهیم این است که در صورتی که فایل‌های js ما زیاد شوند، فایل manifest.jason زیادی شلوغ شده و شکل زشتی پیدا می‌کند و بهتر است این فایل را تا آنجا که می‌توانیم خلاصه نگه داریم. البته روش بالا برای دو یا سه تا فایل js بسیار خوب است ولی اگر به فرض بشود 10 تا یا بیشتر بهتر است یک فایل جداگانه شود و من به همین علت فایل background.htm را درست کرده و به صورت زیر تعریف کرده‌ام:
نکته:نمی توان در تعریف بک گراند هم فایل اسکریپت معرفی کرد و هم فایل html
"background": {
    "page": "background.htm"
}
background.htm
<html>
  <head>
  <script type="text/javascript" src="const.js"></script>
    <script type="text/javascript" src="https://www.google.com/jsapi"></script>
    <script type="text/javascript" src="init.js"></script>
<script type="text/javascript" src="omnibox.js"></script>
<script type="text/javascript" src="rssreader.js"></script>
<script type="text/javascript" src="contextmenus.js"></script>
  </head>
  <body>
  </body>
</html>
لینک‌های بالا به ترتیب معرفی ثابت‌ها، لینک api گوگل که بعدا بررسی می‌شود، فایل init.js برای ذخیره مقادیر پیش فرض، فایل ominibox که در مقاله پیشین در مورد آن صحبت کردیم و فایل rssreader.js که جهت خواندن rss در پایینتر در موردش بحث می‌کنیم و فایل contextmenus که این را هم در مطلب پیشین توضیح دادیم.
جهت خواندن فید سایت ما از Google API استفاده می‌کنیم؛ اینکار دو دلیل دارد:
  1. کدنویسی راحت‌تر و خلاصه‌تر برای خواندن RSS
  2. استفاده اجباری از یک پروکسی به خاطر Content Security Policy و حتی CORS
قبل از اینکه manifst به ورژن 2 برسد ما اجازه داشتیم کدهای جاوااسکریپت به صورت inline در فایل‌های html بنویسیم و یا اینکه از منابع و آدرس‌های خارجی استفاده کنیم برای مثال یک فایل jquery بر روی وب سایت jquery ؛ ولی از ورژن 2 به بعد، گوگل سیاست امنیت محتوا Content Security Policy را که سورس و سند اصلی آن در اینجا قرار دارد، به سیستم Extension خود افزود تا از حملاتی قبیل XSS و یا تغییر منبع راه دور به عنوان یک malware جلوگیری کند. پس ما از این به بعد نه اجازه داشتیم inline بنویسیم و نه اجازه داشتیم فایل jquery را از روی سرورهای سایت سازنده صدا بزنیم. پس برای حل این مشکل، ابتدا مثل همیشه یک فایل js را در فایل html معرفی می‌کردیم و برای حل مشکل دوم باید منابع را به صورت محلی استفاده می‌کردیم؛ یعنی فایل jquery را داخل دایرکتوری extension قرار می‌دادیم.
برای حل مشکل مشکل صدا زدن فایل‌های راه دور ما از Relaxing the Default Policy  استفاده می‌کنیم که به ما یک لیست سفید ارائه می‌کند و در این لیست سفید دو نکته‌ی مهم به چشم میخورد که یکی از آن این است که استفاده از آدرس هایی با پروتکل Https و آدرس لوکال local host/127.0.0.1 بلا مانع است و از آنجا که api گوگل یک آدرس Https است، می‌توانیم به راحتی از API آن استفاده کنیم. فقط نیاز است تا خط زیر را به manifest.json اضافه کنیم تا این استثناء را برای ما در نظر بگیرد.
"content_security_policy": "script-src 'self' https://*.google.com; object-src 'self'"
در اینجا استفاده از هر نوع subdomain در سایت گوگل بلامانع اعلام می‌شود.
بنابراین آدرس زیر به background.htm اضافه می‌شود:
 <script type="text/javascript" src="https://www.google.com/jsapi"></script>

استفاده از این Api در rssreader.js
فایل rssreader.js را به background.htm اضافه می‌کنیم و در آن کد زیر را می‌نویسیم:
google.load("feeds", "1");
google.setOnLoadCallback(alarmManager);
آدرسی که ما از گوگل درخواست کردیم فقط مختص خواندن فید نیست؛ تمامی apiهای جاوااسکریپتی در آن قرار دارند و ما تنها نیاز داریم قسمتی از آن لود شود. پس اولین خط از دستور بالا بارگذاری بخش مورد نیاز ما را به عهده دارد. در مورد این دستور این صفحه را مشاهده کنید.
در خط دوم ما تابع خودمان را به آن معرفی می‌کنیم تا وقتی که گوگل لودش تمام شد این تابع را اجرا کند تا قبل از لود ما از توابع آن استفاده نکنیم و خطای undefined دریافت نکنیم. تابعی که ما از آن خواستیم اجرا کند alarmManager نام دارد و قرار است یک آلارم و یک سیکل زمانی را ایجاد کرده و در هر دوره، فید را بخواند. کد تابع مدنظر به شرح زیر است:
function alarmManager()
{
chrome.storage.local.get(DateContainer.interval,function ( items) {
period_time==items[DateContainer.interval];
chrome.alarms.create('RssInterval', {periodInMinutes: period_time});
});


chrome.alarms.onAlarm.addListener(function (alarm) {
console.log(alarm);
    if (alarm.name == 'RssInterval') {

var boolposts,boolpostsComments,boolshares,boolsharesComments;
chrome.storage.local.get([Variables.posts,Variables.postsComments,Variables.shares,Variables.sharesComments],function ( items) {
boolposts=items[Variables.posts];
boolpostsComments=items[Variables.postsComments];
boolshares=items[Variables.shares];
boolsharesComments=items[Variables.sharesComments];


chrome.storage.local.get([DateContainer.posts,DateContainer.postsComments,DateContainer.shares,DateContainer.sharesComments],function ( items) {

var Vposts=new Date(items[DateContainer.posts]);
var VpostsComments=new Date(items[DateContainer.postsComments]);
var Vshares=new Date(items[DateContainer.shares]);
var VsharesComments=new Date(items[DateContainer.sharesComments]);

if(boolposts){var result=RssReader(Links.postUrl,Vposts,DateContainer.posts,Messages.PostsUpdated);}
if(boolpostsComments){var result=RssReader(Links.posts_commentsUrl,VpostsComments,DateContainer.postsComments,Messages.CommentsUpdated); }
if(boolshares){var result=RssReader(Links.sharesUrl,Vshares,DateContainer.shares,Messages.SharesUpdated);}
if(boolsharesComments){var result=RssReader(Links.shares_CommentsUrl,VsharesComments,DateContainer.sharesComments,Messages.SharesCommentsUpdated);}

});
});
    }
});
}
خطوط اول تابع alarmManager وظیفه‌ی خواندن مقدار interval را که در init.js ذخیره کرده‌ایم، دارند که به طور پیش فرض 10 ذخیره شده است تا تایمر یا آلارم خود را بر اساس آن بسازیم. در خط chrome.alarms.create یک آلارم با نام rssinterval می‌سازد و قرار است هر 10 دقیقه وظایفی که بر دوشش گذاشته می‌شود را اجرا کند (استفاده از api جهت دسترسی به آلارم نیاز به مجوز "alarms" دارد). وظایفش از طریق یک listener که بر روی رویداد chrome.alarms.onAlarm  گذاشته شده است مشخص می‌شود. در خط بعدی مشخص می‌شود که این رویداد به خاطر چه آلارمی صدا زده شده است. البته از آنجا که ما یک آلارم داریم، نیاز چندانی به این کد نیست. ولی اگر پروژه شما حداقل دو آلارم داشته باشد نیاز است مشخص شود که کدام آلارم باعث صدا زدن این رویداد شده است. در مرحله بعد مشخص می‌کنیم که کاربر قصد بررسی چه قسمت‌هایی از سایت را داشته است و در تابع callback آن هم تاریخ آخرین تغییرات هر بخش را می‌خوانیم و در متغیری نگه داری می‌کنیم. هر کدام را جداگانه چک کرده و تابع RssReader را برای هر کدام صدا می‌زنیم. این تابع 4 پارامتر دارد:
  1. آدرس فیدی که قرار است از روی آن بخواند
  2. آخرین به روزسانی که از سایت داشته متعلق به چه تاریخی است.
  3. نام کلید ذخیره سازی تاریخ آخرین تغییر سایت که اگر بررسی شد و مشخص شد سایت به روز شده است، تاریخ جدید را روی آن ذخیره کنیم.
  4. در صورتی که سایت به روز شده باشد نیاز است پیامی را برای کاربر نمایش دهیم که این پیام را در اینجا قرار می‌دهیم.
کد تابع rssreader
function RssReader(URL,lastupdate,datecontainer,Message) {
            var feed = new google.feeds.Feed(URL);
            feed.setResultFormat(google.feeds.Feed.XML_FORMAT);
                    feed.load(function (result) {
if(result!=null)
{
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
var RssUpdate=new Date(strRssUpdate);

if(RssUpdate>lastupdate)
{
SaveDateAndShowMessage(datecontainer,strRssUpdate,Message)
}
}
});
}
در خط اول فید توسط گوگل خوانده میشود، در خط بعدی ما به گوگل میگوییم که فید خوانده شده را چگونه به ما تحویل دهد که ما قالب xml را خواسته ایم و در خط بعدی اطلاعات را در متغیری به اسم result قرار میدهد که در یک تابع برگشتی آن را در اختیار ما میگذارد. از آن جا که ما قرار است تگ lastBuildDate را بخوانیم که پنجمین تگ اولین گره در اولین گره به حساب می‌آید، خط زیر این دسترسی را برای ما فراهم می‌کند و چون تگ ما در یک مکان ثابت است با همین تکه کد، دسترسی مستقیمی به آن داریم:
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
مرحله بعد تاریخ را که در قالب رشته‌ای است، تبدیل به تاریخ کرده و با lastupdate یعنی آخرین تغییر قبلی مقایسه می‌کنیم و اگر تاریخ برگرفته از فید بزرگتر بود، یعنی سایت به روز شده است و تابع SaveDateAndShowMessage را صدا می‌زنیم که وظیفه ذخیره سازی تاریخ جدید و ایجاد notification را به عهده دارد و سه پارامتر کلید ذخیره سازی و مقدار آن و پیام را به آن پاس می‌کنیم.

کد تابع SaveDateAndShowMesage
function SaveDateAndShowMessage(DateField,DateValue,Message)
{
var params={
}
params[DateField]=DateValue;

chrome.storage.local.set( params,function(){

var options={
  type: "basic",
   title: Messages.SiteUpdated,
   message: Message,
   iconUrl: "icon.png"
}
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});
});
});
});
}
خطوط اول مربوط به ذخیره تاریخ است و دومین نکته نحوه‌ی ساخت نوتیفکیشن است. اجرای یک notification  نیاز به مجوز "notifications " دارد که مجوز آن در manifest به شرح زیر است:
"permissions": [
    "storage",
     "tabs",
 "alarms",
 "notifications"
  ]
در خطوط بالا سایر مجوزهایی که در طول این دوره به کار اضافه شده است را هم می‌بینید.
برای ساخت نوتیفکیشن از کد chrome.notifications.create استفاده می‌کنیم که پارامتر اول آن کد یکتا یا همان ID جهت ساخت نوتیفیکیشن هست که میتوان خالی گذاشت و دومی تنظیمات ساخت آن است؛ از قبیل عنوان و آیکن و ... که در بالا به اسم options معرفی کرده ایم و در آگومان دوم آن را معرفی کرده ایم و آرگومان سوم هم یک تابع callback است که نوشتن آن اجباری است. options شامل عنوان، پیام، آیکن و نوع notification می‌باشد که در اینجا basic انتخاب کرده‌ایم. برای دسترسی به دیگر خصوصیت‌های options به اینجا و برای داشتن notification‌های زیباتر به عنوان rich notification به اینجا مراجعه کنید. برای اینکه این امکان باشد که کاربر با کلیک روی notification به سایت هدایت شود باید در تابع callback مربوط به notifications.create این کد اضافه گردد که در صورت کلیک یک تب جدید با آدرس سایت ساخته شود:
chrome.notifications.create("",options,function(){
chrome.notifications.onClicked.addListener(function(){
chrome.tabs.create({'url': WebLinks.Home}, function(tab) {
});});
});

نکته مهم:  پیشتر معرفی آیکن به صورت بالا کفایت میکرد ولی بعد از این باگ  کد زیر هم باید جداگانه به manifest اضافه شود:
"web_accessible_resources": [
    "icon.png"
  ]


خوب؛ کار افزونه تمام شده است ولی اجازه دهید این بار امکانات افزونه را بسط دهیم:
من می‌خواهم برای افزونه نیز قسمت تنظیمات داشته باشم. برای دسترسی به options میتوان از قسمت مدیریت افزونه‌ها در مرورگر یا حتی با راست کلیک روی آیکن browser action عمل کرد. در اصل این قسمت برای تنظیمات افزونه است ولی ما به خاطر آموزش و هم اینکه افزونه ما UI خاصی نداشت تنظیمات را از طریق browser action پیاده سازی کردیم و گرنه در صورتی که افزونه شما شامل UI خاصی مثلا نمایش فید مطالب باشد، بهترین مکان تنظیمات، options است. برای تعریف options در manifest.json به روش زیر اقدام کنید:
"options_page": "popup.html"
همان صفحه popup را در این بخش نشان میدهم و اینبار یک کار اضافه‌تر دیگر که نیاز به آموزش ندارد اضافه کردن input  با Type=number است که برای تغییر interval به کار می‌رود و نحوه ذخیره و بازیابی آن را در طول دوره یاد گرفته اید.

جایزگزینی صفحات یا 
 Override Pages
بعضی صفحات مانند بوک مارک و تاریخچه فعالیت‌ها History و همینطور newtab را می‌توانید جایگزین کنید. البته یک اکستنشن میتواند فقط یکی از صفحات را جایگزین کند. برای تعیین جایگزین در manifest اینگونه عمل می‌کنیم:
"chrome_url_overrides": {
    "newtab": "newtab.htm"
  }

ایجاد یک تب اختصاصی در Developer Tools
تکه کدی که باید manifest اضافه شود:
"devtools_page": "devtools.htm"
شاید فکر کنید کد بالا الان شامل مباحث ui و ... می‌شود و بعد به مرورگر اعمال خواهد شد؛ در صورتی که اینگونه نیست و نیاز دارد چند خط کدی نوشته شود. ولی مسئله اینست که کد بالا تنها صفحات html را پشتیبانی می‌کند و مستقیما نمی‌تواند فایل js را بخواند. پس صفحه بالا را ساخته و کد زیر را داخلش می‌گذاریم:
<script src="devtools.js"></script>
فایل devtools.js هم شامل کد زیر می‌شود:
chrome.devtools.panels.create(
    "Dotnettips Updater Tools", 
    "icon.png", 
    "devtoolsui.htm",
    function(panel) {
    }
);
خط chrome.devtools.panels.create یک پنل یا همان تب را ساخته و در پارامترهای بالا به ترتیب عنوان، آیکن و صفحه‌ای که باید در آن رندر شود را دریافت می‌کند و پس از ایجاد یک callback اجرا می‌شود. اطلاعات بیشتر

APIها
برای دیدن لیست کاملی از API‌ها می‌توانید به مستندات آن رجوع کنید و این مورد را به یاد داشته باشید که ممکن است بعضی api‌ها در بعضی موارد پاسخ ندهند. به عنوان مثال در content scripts نمی‌توانید به chrome.devtools.panels دسترسی داشته باشید یا اینکه در DeveloperTools  دسترسی به DOM میسر نیست. پس این مورد را به خاطر داشته باشید. همچنین بعضی api‌ها از نسخه‌ی خاصی به بعد اضافه شده‌اند مثلا همین مثال قبلی devtools از نسخه 18 به بعد اضافه شده است و به این معنی است با خیال راحت می‌توانید از آن استفاده کنید. یا آلارم‌ها از نسخه 22 به بعد اضافه شده‌اند. البته خوشبختانه امروزه با دسترسی آسانتر به اینترنت و آپدیت خودکار مرورگرها این مشکلات دیگر آن چنان رخ نمی‌دهند.

Messaging
همانطور که در بالا اشاره شد شما نمی‌توانید بعضی از apiها را در بعضی جاها استفاده کنید. برای حل این مشکل می‌توان از messaging استفاده کرد که دو نوع تبادلات پیغامی داریم:
  1. One-Time Requests یا درخواست‌های تک مرتبه‌ای
  2. Long-Lived Connections یا اتصالات بلند مدت یا مصر

درخواست‌های تک مرتبه ای
  این درخواست‌ها همانطور که از نامش پیداست تنها یک مرتبه رخ می‌دهد؛ درخواست را ارسال کرده و منتظر پاسخ می‌ماند. به عنوان مثال به کد زیر که در content script است دقت کنید:
window.addEventListener("load", function() {
    chrome.extension.sendMessage({
        type: "dom-loaded", 
        data: {
            myProperty   : "value" 
        }
    });
}, true);
کد بالا یک ارسال کننده پیام است. موقعی که سایتی باز می‌شود، یک کلید با مقدارش را ارسال می‌کند و کد زیر در background گوش می‌ایستد تا اگر درخواستی آمد آن را دریافت کند:
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
    switch(request.type) {
        case "dom-loaded":
            alert(request.data.myProperty   ); 
        break;
    }
    return true;
});

اتصالات بلند مدت یا مصر
اگر نیاز به یک کانال ارتباطی مصر و همیشگی دارید کد‌ها را به شکل زیر تغییر دهید
contentscripts
var port = chrome.runtime.connect({name: "my-channel"});
port.postMessage({myProperty: "value"});
port.onMessage.addListener(function(msg) {
    // do some stuff here
});

background
chrome.runtime.onConnect.addListener(function(port) {
    if(port.name == "my-channel"){
        port.onMessage.addListener(function(msg) {
            // do some stuff here
        });
    }
});

نمونه کد
نمونه کدهایی که در سایت گوگل موجود هست می‌توانند کمک بسیاری خوبی باشند ولی اینگونه که پیداست اکثر مثال‌ها مربوط به نسخه‌ی یک manifest است که دیگر توسط مرورگرها پشتیبانی نمی‌شوند و مشکلاتی چون اسکریپت inline و CSP که در بالا اشاره کردیم را دارند و گوگل کدها را به روز نکرده است.

دیباگ کردن و پک کردن فایل‌ها برای تبدیل به فایل افزونه Debugging and packing
برای دیباگ کردن کد‌ها می‌توان از دو نمونه console.log و alert برای گرفتن خروجی استفاده کرد و همچنین ابزار  Chrome Apps & Extensions Developer Tool هم نسبتا امکانات خوبی دارد که البته میتوان از آن برای پک کردن اکستنشن نهایی هم استفاده کرد. برای پک کردن روی گزینه pack کلیک کرده و در کادر باز شده گزینه‌ی pack را بزنید. برای شما دو نوع فایل را در مسیر والد دایرکتوری extension نوشته شده درست خواهد کرد که یکی پسوند crx دارد که می‌شود همان فایل نهایی افزونه و دیگری هم پسوند pem دارد که یک کلید اختصاصی است و باید برای آپدیت‌های آینده افزونه آن را نگاه دارید. در صورتی که افزونه را تغییر دادید و خواستید آن را به روز رسانی کنید موقعی که اولین گزینه pack را می‌زنید و صفحه باز می‌شود قبل از اینکه دومین گزینه pack را بزنید، از شما می‌خواهد اگر دارید عملیات به روز رسانی را انجام می‌دهید، کلید اختصاصی آن را وارد نمایید و بعد از آن گزینه pack را بزنید:


آپلود نهایی کار در Google web store

برای آپلود نهایی کار به google web store که در آن تمامی برنامه‌ها و افزونه‌های کروم قرار دارند بروید. سمت راست آیکن تنظیمات را بزنید و گزینه developer dashboard را انتخاب کنید تا صفحه‌ی آپلود کار برای شما باز شود. دایرکتوری محتویات اکستنشن را zip کرده و آپلود نمایید. توجه داشته باشید که محتویات و سورس خود را باید آپلود کنید نه فایل crx را. بعد از آپلود موفقیت آمیز، صفحه‌ای ظاهر می‌شود که از شما آیکن افزونه را در اندازه 128 پیکسل میخواهد بعلاوه توضیحاتی در مورد افزونه، قیمت گذاری که به طور پیش فرض به صورت رایگان تنظیم شده است، لینک وب سایت مرتبط، لینک محل پرسش و پاسخ برای افزونه، اگر لینک یوتیوبی در مورد افزونه دارید، یک شات تصویری از افزونه و همینطور چند تصویر برای اسلایدشو سازی که در همان صفحه استاندارد آن‌ها را توضیح می‌دهد و در نهایت گزینه‌ی جالب‌تر هم اینکه اکستنشن شما برای چه مناطقی تهیه شده است که متاسفانه ایران را ندیدم که می‌توان همه موارد را انتخاب کرد. به خصوص در مورد ایران که آی پی‌ها هم صحیح نیست، انتخاب ایران چنان تاثیری ندارد و در نهایت گزینه‌ی publish را می‌زنید که متاسفانه بعد از این صفحه درخواست می‌کند برای اولین بار باید 5 دلار آمریکا پرداخت شود که برای بسیاری از ما این گزینه ممکن نیست.

سورس پروژه را می‌توانید از اینجا ببینید و خود افزونه را از اینجا دریافت کنید.

 
نظرات مطالب
کنترل DatePicker شمسی مخصوص Silverlight 4
سلام،
برای WPF می‌تونید از مواردی که نامبرده شد استفاده کنید.
+ این کامپوننت نیاز هست کمی تغییر کند تا برای WPF هم قابل استفاده شود. WPF خیلی از مواردی را که در آخر بحث فوق ذکر شد را دارد (مانند uniform grid ، تقویم شمسی و غیره). این‌ها باید حذف شوند. رفتار popup در wpf بسیار پخته‌تر است و کاملتر و این مورد هم باید کمی تعدیل شود. مابقی موارد با یک کامپایل در پروژه‌ی جدید مخصوص WPF قابل استفاده خواهد بود. مثال‌های آن هم تفاوتی نمی‌کند.
وقت کنم به زودی این رو به WPF هم انتقال می‌دم.
مطالب
فرم‌های مبتنی بر قالب‌ها در Angular - قسمت پنجم - ارسال اطلاعات به سرور
تا اینجا تنظیمات اصلی فرم ثبت اطلاعات کارمندان را انجام دادیم. اکنون نوبت به ارسال این اطلاعات به سمت سرور است. پیشنیاز آن نیز تدارک مواردی است که در مطلب «یکپارچه سازی Angular CLI و ASP.NET Core در VS 2017» پیشتر بحث شدند. از این مطلب تنها تنظیمات موارد ذیل را نیاز خواهیم داشت و از تکرار آن‌ها در اینجا صرفنظر می‌شود تا هم مطلب کوتاه‌تر شود و هم بتوان بر روی اصل موضوع جاری، تمرکز کرد:
- ایجاد یک پروژه‌ی جدید ASP.NET Core در VS 2017
- تنظیمات یک برنامه‌ی ASP.NET Core خالی برای اجرای یک برنامه‌ی Angular CLI
- تنظیمات فایل آغازین یک برنامه‌ی ASP.NET Core جهت ارائه‌ی برنامه‌های Angular
- ایجاد ساختار اولیه‌ی برنامه‌ی Angular CLI در داخل پروژه‌ی جاری: این مورد را تاکنون انجام داده‌ایم و تکمیل کرده‌ایم. بنابراین تنها کاری که نیاز است انجام شود، cut و paste محتوای پوشه‌ی angular-template-driven-forms-lab (پروژه‌ی این سری) به ریشه‌ی پروژه‌ی ASP.NET Core است.
- تنظیم محل خروجی نهایی Angular CLI به پوشه‌ی wwwroot
- روش اول و یا دوم اجرای برنامه‌های مبتنی بر ASP.NET Core و Angular CLI

البته سورس کامل تمام این تنظیمات را از انتهای بحث نیز می‌توانید دریافت کنید.
ضمن اینکه هیچ نیازی هم به استفاده از VS 2017 نیست و هر دوی برنامه‌ی Angular و ASP.NET Core را می‌توان توسط VSCode به خوبی مدیریت و اجرا کرد.


ایجاد ساختار مقدماتی سرویس ارسال اطلاعات به سرور

در برنامه‌های Angular مرسوم است جهت کاهش مسئولیت‌های یک کلاس و امکان استفاده‌ی مجدد از کدها، منطق ارسال اطلاعات به سرور، به درون کلاس یک سرویس منتقل شود و سپس این سرویس به کلاس‌های کامپوننت‌ها، برای مثال یک فرم ثبت اطلاعات، برای ارسال و یا دریافت اطلاعات، تزریق گردد. به همین جهت، ابتدا ساختار ابتدایی این سرویس و تنظیمات مرتبط با آن‌را انجام می‌دهیم.
ابتدا از طریق خط فرمان به پوشه‌ی ریشه‌ی برنامه وارد شده (جائیکه فایل Startup.cs قرار دارد) و سپس دستور ذیل را اجرا می‌کنیم:
 >ng g s employee/FormPoster -m employee.module
با این خروجی
 installing service
  create src\app\employee\form-poster.service.spec.ts
  create src\app\employee\form-poster.service.ts
  update src\app\employee\employee.module.ts
همانطور که در سطر آخر نیز ملاحظه می‌کنید، فایل employee.module.ts را جهت درج کلاس جدید FormPosterService در قسمت providers ماژول آن به روز رسانی می‌کند؛ تا بتوانیم این سرویس را در کامپوننت‌های این ماژول تزریق کرده و استفاده کنیم.
ساختار ابتدایی این سرویس را نیز به نحو ذیل تغییر می‌دهیم:
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';

import { Employee } from './employee';

@Injectable()
export class FormPosterService {

    constructor(private http:Http) {
    }

    postEmployeeForm(employee: Employee) {
    }
}
در اینجا سرویس Http انگیولار به سازنده‌ی کلاس تزریق شده‌است و این نحوه‌ی تعریف سبب می‌شود تا بتوان به پارامتر http، به صورت یک فیلد خصوصی تعریف شده‌ی در سطح کلاس نیز دسترسی پیدا کنیم.
چون این کلاس از ماژول توکار Http استفاده می‌کند، نیاز است این ماژول را نیز به قسمت imports فایل src\app\app.module.ts اضافه کنیم:
import { HttpModule } from "@angular/http";

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    EmployeeModule,
    AppRoutingModule
  ]
اکنون می‌توانیم این سرویس جدید FormPosterService را به سازنده‌ی کامپوننت EmployeeRegisterComponent در فایل src\app\employee\employee-register\employee-register.component.ts تزریق کنیم:
import { FormPosterService } from "../form-poster.service";

export class EmployeeRegisterComponent implements OnInit {

  constructor(private formPoster: FormPosterService) {}

}

در ادامه برای آزمایش برنامه، به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی، دستورات:
>npm install
>ng build --watch
و در دومی، دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
دستورات اول کار بازیابی وابستگی‌های سمت کلاینت و سپس ساخت تدریجی برنامه‌ی Angular را دنبال می‌کند. دستورات دوم، وابستگی‌های برنامه‌ی ASP.NET Core را دریافت و نصب کرده و سپس برنامه را در حالت watch ساخته و بر روی پورت 5000 ارائه می‌کند (بدون نیاز به اجرای VS 2017؛ این دستور عمومی است).
به همین جهت برای آزمایش ابتدایی آن، آدرس http://localhost:5000 را در مرورگر باز کنید. برگه‌ی developer tools مرورگر را نیز بررسی کنید تا خطایی در آن ظاهر نشده باشد. برای مثال اگر فراموش کرده باشید تا HttpModule را به app.module اضافه کنید، خطای no provider for HttpModule را مشاهده خواهید کرد.


مدیریت رخداد submit فرم در Angular

تا اینجا کار برپایی تنظیمات اولیه‌ی کار با سرویس Http را انجام دادیم. مرحله‌ی بعد مدیریت رخداد submit فرم است. به همین جهت فایل src\app\employee\employee-register\employee-register.component.html را گشوده و سپس رخدادگردان submit را به فرم آن اضافه کنید:
<form #form="ngForm" (submit)="submitForm(form)" novalidate>
در حین رخدادگردانی submit می‌توان به template reference variable تعریف شده‌ی form# برای دسترسی به وهله‌ای از ngForm نیز کمک گرفت.
export class EmployeeRegisterComponent implements OnInit {
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);
  }
}
امضای متد submitForm را در اینجا مشاهده می‌کنید. form دریافتی آن از نوع NgForm است که در ابتدای فایل import شده‌است.
در همین حال اگر بر روی دکمه‌ی ok کلیک کنیم، چنین خروجی را در کنسول developer مروگر می‌توان مشاهده کرد:


اولین مورد، محتوای this.model است و دومی محتوای form.value را گزارش کرده‌است. همانطور که مشاهده می‌کنید، مقدار form.value بسیار شبیه است به وهله‌ای از مدلی که در سطح کلاس تعریف کرده‌ایم و این مقدار همواره توسط Angular نگهداری و مدیریت می‌شود. بنابراین حتما الزامی نیست تا مدلی را جهت کار با فرم‌های مبتنی بر قالب‌ها به صورت جداگانه‌ای تهیه کرد. توسط شیء form نیز می‌توان به تمام اطلاعات فیلدها دسترسی یافت.


تکمیل سرویس ارسال اطلاعات به سرور

در ادامه می‌خواهیم اطلاعات مدل فرم را به سرور ارسال کنیم. برای این منظور سرویس FormPoster را به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { Http, Response, Headers, RequestOptions } from "@angular/http";

import { Observable } from "rxjs/Observable";
import "rxjs/add/operator/do";
import "rxjs/add/operator/catch";
import "rxjs/add/observable/throw";
import "rxjs/add/operator/map";
import "rxjs/add/observable/of";

import { Employee } from "./employee";

@Injectable()
export class FormPosterService {
  private baseUrl = "api/employee";

  constructor(private http: Http) {}

  private extractData(res: Response) {
    const body = res.json();
    return body.fields || {};
  }

  private handleError(error: Response): Observable<any> {
    console.error("observable error: ", error);
    return Observable.throw(error.statusText);
  }

  postEmployeeForm(employee: Employee): Observable<Employee> {
    const body = JSON.stringify(employee);
    const headers = new Headers({ "Content-Type": "application/json" });
    const options = new RequestOptions({ headers: headers });

    return this.http
      .post(this.baseUrl, body, options)
      .map(this.extractData)
      .catch(this.handleError);
  }
}
برای کار با Observables یا می‌توان نوشت 'import 'rxjs/Rx که تمام بسته‌ی RxJS را import می‌کند، یا همانند این مثال بهتر است تنها اپراتورهایی را که به آن‌ها نیاز پیدا می‌کنیم، import نمائیم. به این ترتیب حجم نهایی ارائه‌ی برنامه نیز کاهش خواهد یافت.
در متد postEmployeeForm، ابتدا توسط JSON.stringify محتوای شیء کارمند encode می‌شود. البته متد post اینکار را به صورت توکار نیز می‌تواند مدیریت کند. سپس ذکر هدر مناسب در اینجا الزامی است تا در سمت سرور بتوانیم اطلاعات دریافتی را به شیء متناظری نگاشت کنیم. در غیراینصورت model binder سمت سرور نمی‌داند که چه نوع فرمتی را دریافت کرده‌است و چه نوع decoding را باید انجام دهد.
در قسمت map، کار بررسی اطلاعات دریافتی از سرور را انجام خواهیم داد و اگر در این بین خطایی وجود داشت، توسط متد handleError در کنسول developer مرورگر نمایش داده می‌شود.
خروجی متد postEmployeeForm یک Observable است. بنابراین تا زمانیکه یک subscriber نداشته باشد، اجرا نخواهد شد. به همین جهت به کلاس EmployeeRegisterComponent مراجعه کرده و متد submitForm را به نحو ذیل تکمیل می‌کنیم:
  submitForm(form: NgForm) {
    console.log(this.model);
    console.log(form.value);

    // validate form
    this.validatePrimaryLanguage(this.model.primaryLanguage);
    if (this.hasPrimaryLanguageError) {
      return;
    }

    this.formPoster
      .postEmployeeForm(this.model)
      .subscribe(
        data => console.log("success: ", data),
        err => console.log("error: ", err)
      );
  }
در اینجا ابتدا اعتبارسنجی سفارشی drop down را که در قسمت قبل بررسی کردیم، قرار داده‌ایم. پس از آن متد postEmployeeForm سرویس formPoster فراخوانی شده‌است و در اینجا کار subscribe به نتیجه‌ی عملیات صورت گرفته‌است که می‌تواند حاوی اطلاعاتی از سمت سرور و یا خطایی در این بین باشد.

یک نکته: اگر علاقمند باشید تا ساختار واقعی شیء NgForm را مشاهده کنید، در ابتدای متد فوق، console.log(form.form) را فراخوانی کنید و سپس شیء حاصل را در کنسول developer مرورگر بررسی نمائید.


تکمیل Web API برنامه‌ی ASP.NET Core جهت دریافت اطلاعات از کلاینت‌ها

در ابتدای سرویس formPoster، یک چنین تعریفی را داریم:
export class FormPosterService {
  private baseUrl = "api/employee";
به همین جهت نیاز است سرویس Web API سمت سرور خود را بر این مبنا تکمیل کنیم.
ابتدا مدل زیر را به پروژه‌ی ASP.NET Core جاری، معادل نمونه‌ی تایپ‌اسکریپتی سمت کلاینت آن اضافه می‌کنیم. البته در اینجا یک Id نیز اضافه شده‌است:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool IsFullTime { get; set; }
        public string PaymentType { get; set; }
        public string PrimaryLanguage { get; set; }
    }
}

سپس کنترلر جدید EmployeeController را با محتوای ذیل اضافه خواهیم کرد:
using Microsoft.AspNetCore.Mvc;
using AngularTemplateDrivenFormsLab.Models;

namespace AngularTemplateDrivenFormsLab.Controllers
{
    [Route("api/[controller]")]
    public class EmployeeController : Controller
    {
        public IActionResult Post([FromBody] Employee model)
        {
            //todo: save model

            model.Id = 100;
            return Created("", new { fields = model });
        }
    }
}
این کنترلر با شیوه‌ی Web API تعریف شده‌است. مسیریابی آن با api شروع می‌شود تا با مسیر baseUrl سرویس formPoster تطابق پیدا کند.
در اینجا پس از ثبت فرضی مدل، Id آن به همراه اطلاعات مدل، به نحوی که ملاحظه می‌کنید، بازگشت داده شده‌است. این نوع خروجی، یک چنین JSON ایی را تولید می‌کند:
{"fields":{"id":100,"firstName":"Vahid","lastName":"N","isFullTime":true,"paymentType":"FullTime","primaryLanguage":"Persian"}}
به همین جهت است که در متد extractData، دسترسی به body.fields را مشاهده می‌کنید. این fields در اینجا دربرگیرنده‌ی اطلاعات بازگشتی از سرور است (نام آن دلخواه است و درصورت تغییر آن در سمت سرو، باید این نام را در متد extractData نیز اصلاح کنید).
  private extractData(res: Response) {
    const body = res.json();
    return body.fields || {};
  }
اکنون اگر برنامه را با دستورات dotnet watch build و ng build --watch اجرا کنیم، بر روی پورت 5000 قابل دسترسی خواهد بود و پس از ارسال فرم به سرور، چنین خروجی را می‌توان در کنسول developer مرورگر مشاهده کرد:


نمایش success به همراه شیءایی که از سمت سرور دریافت شده‌است؛ که حاصل اجرای سطر ذیل در متد submitForm است:
 data => console.log("success: ", data)
همانطور که مشاهده می‌کنید، این شیء به همراه Id نیز هست. بنابراین درصورت نیاز به آن در سمت کلاینت، خاصیت معادل آن‌را به کلاس کارمند اضافه کرده و در همین سطر فوق می‌توان به آن دسترسی یافت.


بارگذاری اطلاعات drop down از سرور

تا اینجا اطلاعات drop down نمایش داده شده از یک آرایه‌ی مشخص سمت کلاینت تامین شدند. در ادامه قصد داریم تا آن‌ها را از سرور دریافت کنیم. به همین جهت اکشن متد ذیل را به کنترلر سمت سرور برنامه اضافه کنید:
[HttpGet("/api/[controller]/[action]")]
public IActionResult Languages()
{
    string[] languages = { "Persian", "English", "Spanish", "Other" };
    return Ok(languages);
}
که برای آزمایش آن می‌توانید مسیر http://localhost:5000/api/employee/languages را جداگانه در مرورگر درخواست کنید.
پس از آن در سمت کلاینت این تغییرات نیاز هستند:
ابتدا به سرویس FormPosterService دو متد ذیل را اضافه می‌کنیم که کار آن‌ها دریافت و پردازش اطلاعات از api/employee/languages سمت سرور هستند:
  private extractLanguages(res: Response) {
    const body = res.json();
    return body || {};
  }

  getLanguages(): Observable<any> {
    return this.http
      .get(`${this.baseUrl}/languages`)
      .map(this.extractLanguages)
      .catch(this.handleError);
  }
اینبار چون خروجی سمت سرور را مانند قبل (متد extractData) داخل فیلدی مانند fields محصور نکردیم، همان body دریافتی بازگشت داده شده‌است.
پس از آن دو تغییر ذیل را نیاز است به EmployeeRegisterComponent اعمال کنیم:
  languages = [];

  ngOnInit() {
    this.formPoster
      .getLanguages()
      .subscribe(
        data => this.languages = data,
        err => console.log("get error: ", err)
      );
  }
ابتدا آرایه‌ی زبان‌ها با یک آرایه‌ی خالی مقدار دهی شده‌است و سپس در متد ngOnInit، کار دریافت اطلاعات آن از سرور، صورت گرفته‌است.

مشکل! ممکن است مدت زمانی طول بکشد تا این اطلاعات از سمت سرور دریافت شوند. در این حالت می‌توان به شکل زیر در فایل employee-register.component.html فرم را تا زمان پر شدن دراپ داون آن مخفی کرد:
<h3 *ngIf="languages.length == 0">Loading...</h3>
<div class="container" *ngIf="languages.length > 0">
در این حالت هر زمانیکه آرایه‌ی زبان‌ها پر شد، loading حذف شده و div نمایان می‌گردد.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-template-driven-forms-lab-05.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس به ریشه‌ی پروژه وارد شده و دو پنجره‌ی کنسول مجزا را باز کنید. در اولی دستورات:
>npm install
>ng build --watch
و در دومی دستورات ذیل را اجرا کنید:
>dotnet restore
>dotnet watch run
اکنون می‌توانید برنامه را در آدرس http://localhost:5000 مشاهده و اجرا کنید.
مطالب
روش آپلود فایل‌ها به همراه اطلاعات یک مدل در برنامه‌های Blazor WASM 5x
از زمان Blazor 5x، امکان آپلود فایل به صورت استاندارد به Blazor اضافه شده‌است که نمونه‌ی Blazor Server آن‌را پیشتر در مطلب «Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر» مطالعه کردید. در تکمیل آن، روش آپلود فایل‌ها در برنامه‌های WASM را نیز بررسی خواهیم کرد. این برنامه از نوع hosted است؛ یعنی توسط دستور dotnet new blazorwasm --hosted ایجاد شده‌است و به صورت خودکار دارای سه بخش Client، Server و Shared است.



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

فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کرده‌ایم:
using System.ComponentModel.DataAnnotations;

namespace BlazorWasmUpload.Shared
{
    public class User
    {
        [Required]
        public string Name { get; set; }

        [Required]
        [Range(18, 90)]
        public int Age { get; set; }
    }
}

ساختار کنترلر Web API دریافت کننده‌ی مدل برنامه

در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> CreateUser(
            [FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
یعنی در سمت Web API، قرار است اطلاعات مدل User و همچنین لیستی از فایل‌های آپلودی (احتمالی و اختیاری) را یکجا و در طی یک عملیات Post، دریافت کنیم. در اینجا نام پارامترهایی را هم که انتظار داریم، دقیقا userModel و inputFiles هستند. همچنین فایل‌های آپلودی باید بتوانند ساختار IFormFile استاندارد ASP.NET Core را تشکیل داده و به صورت خودکار به پارامترهای تعریف شده، bind شوند. به علاوه content-type مورد انتظار هم FromForm است.


ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایل‌های انتخابی کاربر

کدهای کامل سرویسی که می‌تواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorWasmUpload.Client.Services
{
    public interface IFilesManagerService
    {
        Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName);
    }

    public class FilesManagerService : IFilesManagerService
    {
        private readonly HttpClient _httpClient;

        public FilesManagerService(HttpClient httpClient)
        {
            _httpClient = httpClient;
        }

        public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>(
            string requestUri,
            IEnumerable<IBrowserFile> browserFiles,
            string fileParameterName,
            T model,
            string modelParameterName)
        {
            var requestContent = new MultipartFormDataContent();
            requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");

            if (browserFiles?.Any() == true)
            {
                foreach (var file in browserFiles)
                {
                    var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000);
                    requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name);
                }
            }

            requestContent.Add(
                content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"),
                name: modelParameterName);

            var result = await _httpClient.PostAsync(requestUri, requestContent);
            result.EnsureSuccessStatusCode();
            return result;
        }
    }
}
توضیحات:
- کامپوننت استاندارد InputFiles در Blazor Wasm، می‌تواند لیستی از IBrowserFile‌های انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.
- model جنریک، برای نمونه وهله‌ای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شده‌است.

- در ادامه یک MultipartFormDataContent را تشکیل داده‌ایم. توسط این ساختار می‌توان فایل‌ها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContent‌ها را اضافه می‌کنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل می‌شوند. متد OpenReadStream به صورت پیش‌فرض فقط فایل‌هایی تا حجم 500 کیلوبایت را پردازش می‌کند و اگر فایلی حجیم‌تر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت می‌توان توسط پارامتر maxAllowedSize آن، این مقدار پیش‌فرض را تغییر داد.

- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شده‌است. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشته‌ی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشته‌ی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی می‌توان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال می‌شود؛ چون MultipartFormDataContent ویژه‌ای را داریم، model-binder پیش‌فرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.

- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده می‌کنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.

- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.

پس از تهیه‌ی سرویس ویژه‌ی فوق که می‌تواند اطلاعات فایل‌ها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگی‌ها در فایل Program.cs برنامه‌ی کلاینت است:
namespace BlazorWasmUpload.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddScoped<IFilesManagerService, FilesManagerService>();

            // ...
        }
    }
}


تکمیل فرم ارسال اطلاعات مدل و فایل‌های همراه آن در برنامه‌ی Blazor WASM

در ادامه پس از تشکیل IFilesManagerService، نوبت به استفاده‌ی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر می‌دهیم:
@code
{
    IReadOnlyList<IBrowserFile> SelectedFiles;
    User UserModel = new User();
    bool isProcessing;
    string UploadErrorMessage;
در اینجا فیلدهای مورد استفاده‌ی در فرم برنامه مشخص شده‌اند:
- SelectedFiles همان لیست فایل‌های انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص می‌کنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایل‌ها مانند «فقط تصاویر را انتخاب کنید» را تعریف می‌کند.

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

@using System.IO
@using BlazorWasmUpload.Shared
@using BlazorWasmUpload.Client.Services

@inject IFilesManagerService FilesManagerService

<h3>Post a model with files</h3>

<EditForm Model="UserModel" OnValidSubmit="CreateUserAsync">
    <DataAnnotationsValidator />
    <div>
        <label>Name</label>
        <InputText @bind-Value="UserModel.Name"></InputText>
        <ValidationMessage For="()=>UserModel.Name"></ValidationMessage>
    </div>
    <div>
        <label>Age</label>
        <InputNumber @bind-Value="UserModel.Age"></InputNumber>
        <ValidationMessage For="()=>UserModel.Age"></ValidationMessage>
    </div>
    <div>
        <label>Photos</label>
        <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" />
        @if (!string.IsNullOrWhiteSpace(UploadErrorMessage))
        {
            <div>
                @UploadErrorMessage
            </div>
        }
        @if (SelectedFiles?.Count > 0)
        {
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Size (bytes)</th>
                        <th>Last Modified</th>
                        <th>Type</th>
                    </tr>
                </thead>
                <tbody>
                    @foreach (var selectedFile in SelectedFiles)
                    {
                        <tr>
                            <td>@selectedFile.Name</td>
                            <td>@selectedFile.Size</td>
                            <td>@selectedFile.LastModified</td>
                            <td>@selectedFile.ContentType</td>
                        </tr>
                    }
                </tbody>
            </table>
        }
    </div>
    <div>
        <button disabled="@isProcessing">Create user</button>
    </div>
</EditForm>
توضیحات:
- UserModel که وهله‌ی از شیء اشتراکی User است، به EditForm متصل شده‌است.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت می‌کنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر می‌کند. پس از انتخاب فایل‌ها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
    private void OnInputFileChange(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 15);
        if (args.FileCount == 0 || files.Count == 0)
        {
            UploadErrorMessage = "Please select a file.";
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            UploadErrorMessage = "Please select .jpg/.jpeg/.png files only.";
            return;
        }

        SelectedFiles = files;
        UploadErrorMessage = string.Empty;
    }
- در اینجا امضای متد رویداد گردان OnChange را مشاهده می‌کنید. توسط متد GetMultipleFiles می‌توان لیست فایل‌های انتخابی توسط کاربر را دریافت کرد. نیاز است پارامتر maximumFileCount آن‌را نیز تنظیم کنیم تا دقیقا مشخص شود چه تعداد فایلی مدنظر است؛ بیش از آن، یک استثناء را صادر می‌کند.
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش می‌دهیم.
- در پایان این متد، لیست فایل‌های دریافتی را به فیلد SelectedFiles انتساب می‌دهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.

مرحله‌ی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
    private async Task CreateUserAsync()
    {
        try
        {
            isProcessing = true;
            await FilesManagerService.PostModelWithFilesAsync(
                        requestUri: "api/Files/CreateUser",
                        browserFiles: SelectedFiles,
                        fileParameterName: "inputFiles",
                        model: UserModel,
                        modelParameterName: "userModel");
            UserModel = new User();
        }
        finally
        {
            isProcessing = false;
            SelectedFiles = null;
        }
    }
- در اینجا زمانیکه isProcessing به true تنظیم می‌شود، دکمه‌ی ارسال اطلاعات، غیرفعال خواهد شد؛ تا از کلیک چندباره‌ی بر روی آن جلوگیری شود.
- سپس روش استفاده‌ی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده می‌کنید که اطلاعات فایل‌ها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال می‌کند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آماده‌ی دریافت اطلاعات بعدی می‌کنیم.


تکمیل کنترلر Web API دریافت کننده‌ی مدل برنامه

در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کننده‌ی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آن‌را مشاهده می‌کنید:
using System.IO;
using Microsoft.AspNetCore.Mvc;
using BlazorWasmUpload.Shared;
using Microsoft.AspNetCore.Hosting;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using System.Text.Json;
using BlazorWasmUpload.Server.Utils;
using System.Linq;

namespace BlazorWasmUpload.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]/[action]")]
    public class FilesController : ControllerBase
    {
        private const int MaxBufferSize = 0x10000;

        private readonly IWebHostEnvironment _webHostEnvironment;
        private readonly ILogger<FilesController> _logger;

        public FilesController(
            IWebHostEnvironment webHostEnvironment,
            ILogger<FilesController> logger)
        {
            _webHostEnvironment = webHostEnvironment;
            _logger = logger;
        }

        [HttpPost]
        public async Task<IActionResult> CreateUser(
            //[FromForm] string userModel, // <-- this is the actual form of the posted model
            [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
            [FromForm] IList<IFormFile> inputFiles = null)
        {
            /*var user = JsonSerializer.Deserialize<User>(userModel);
            _logger.LogInformation($"userModel.Name: {user.Name}");
            _logger.LogInformation($"userModel.Age: {user.Age}");*/

            _logger.LogInformation($"userModel.Name: {userModel.Name}");
            _logger.LogInformation($"userModel.Age: {userModel.Age}");

            var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            if (inputFiles?.Any() == true)
            {
                foreach (var file in inputFiles)
                {
                    if (file == null || file.Length == 0)
                    {
                        continue;
                    }

                    var filePath = Path.Combine(uploadsRootFolder, file.FileName);
                    using var fileStream = new FileStream(filePath,
                                                            FileMode.Create,
                                                            FileAccess.Write,
                                                            FileShare.None,
                                                            MaxBufferSize,
                                                            useAsync: true);
                    await file.CopyToAsync(fileStream);
                    _logger.LogInformation($"Saved file: {filePath}");
                }
            }

            return Ok();
        }
    }
}
نکات تکمیلی این کنترلر را در مطلب «بررسی روش آپلود فایل‌ها در ASP.NET Core» می‌توانید مطالعه کنید و از این لحاظ هیچ نکته‌ی جدیدی را به همراه ندارد؛ بجز پارامتر userModel آن:
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
همانطور که عنوان شد، userModel ارسالی به سمت سرور چون به همراه تعدادی فایل است، به صورت خودکار به شیء User نگاشت نخواهد شد. به همین جهت نیاز است model-binder سفارشی زیر را برای آن تهیه کرد:
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace BlazorWasmUpload.Server.Utils
{
    public class JsonModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }

            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult != ValueProviderResult.None)
            {
                bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

                var valueAsString = valueProviderResult.FirstValue;
                var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType);
                if (result != null)
                {
                    bindingContext.Result = ModelBindingResult.Success(result);
                    return Task.CompletedTask;
                }
            }

            return Task.CompletedTask;
        }
    }
}
در اینجا مقدار رشته‌ای پارامتر مزین شده‌ی توسط JsonModelBinder فوق، توسط متد استاندارد JsonSerializer.Deserialize تبدیل به یک شیء شده و به آن پارامتر انتساب داده می‌شود. اگر نخواهیم از این model-binder سفارشی استفاده کنیم، ابتدا باید پارامتر دریافتی را رشته‌ای تعریف کنیم و سپس خودمان کار فراخوانی متد JsonSerializer.Deserialize را انجام دهیم:
[HttpPost]
public async Task<IActionResult> CreateUser(
            [FromForm] string userModel, // <-- this is the actual form of the posted model
            [FromForm] IList<IFormFile> inputFiles = null)
{
  var user = JsonSerializer.Deserialize<User>(userModel);


یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمی‌شود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه می‌شود و این API استاندارد، هنوز از streaming پشتیبانی نمی‌کند . حتی ممکن است با کمی جستجو به راه‌حل‌هایی که سعی کرده‌اند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راه‌حل‌ها تنها کاری را که انجام می‌دهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده می‌شود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmUpload.zip