مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت چهارم
در قسمت قبل، اطلاعات نمایش داده شده، از یک سری آرایه ثابت جاوا اسکریپتی تامین شدند. در یک برنامه‌ی واقعی نیاز است داده‌ها را یا از HTML 5 local storage تامین کرد و یا از سرور به کمک Ajax. برای اینگونه اعمال، ember.js به همراه افزونه‌ای است به نام Ember Data که جزئیات کار با آن‌را در این قسمت بررسی خواهیم کرد.


استفاده از Ember Data با Local Storage

برای کار با HTML 5 local storage نیاز به Ember Data Local Storage Adapter نیز هست که در قسمت اول این سری، آدرس دریافت آن معرفی شد. این فایل‌ها نیز در پوشه‌ی Scripts\Libs برنامه کپی خواهند شد.
در ادامه به فایل Scripts\App\store.js که در قسمت قبل جهت تعریف دو آرایه ثابت مطالب و نظرات اضافه شد، مراجعه کرده و محتوای فعلی آن‌را با کدهای زیر جایگزین کنید:
Blogger.ApplicationSerializer = DS.LSSerializer.extend();
Blogger.ApplicationAdapter = DS.LSAdapter.extend();
این تعاریف سبب خواهند شد تا Ember Data از Local Storage Adapter استفاده کند.
در ادامه با توجه به حذف دو آرایه‌ی posts و comments که پیشتر در فایل store.js تعریف شده بودند، نیاز است مدل‌های متناظری را جهت تعریف خواص آن‌ها، به برنامه اضافه کنیم. این‌کار را با افزودن دو فایل جدید comment.js و post.js به پوشه‌ی Scripts\Models انجام خواهیم داد.
محتوای فایل Scripts\Models\post.js :
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr()
});
محتوای فایل Scripts\Models\comment.js :
Blogger.Comment = DS.Model.extend({
  text: DS.attr()
});
سپس مداخل تعریف آن‌ها را به فایل index.html نیز اضافه خواهیم کرد:
 <script src="Scripts/Models/post.js" type="text/javascript"></script>
<script src="Scripts/Models/comment.js" type="text/javascript"></script>

برای تعاریف مدل‌ها در Ember data مرسوم است که نام مدل‌ها، اسامی جمع نباشند. سپس با ایجاد وهله‌ای از DS.Model.extend یک مدل ember data را تعریف خواهیم کرد. در این مدل، خواص هر شیء را مشخص کرده و مقدار آن‌ها همیشه ()DS.attr خواهد بود. این نکته را در دو مدل Post و Comment مشاهده می‌کنید. اگر دقت کنید به هر دو مدل، خاصیت id اضافه نشده‌است. این خاصیت به صورت خودکار توسط Ember data تنظیم می‌شود.

اکنون نیاز است برنامه را جهت استفاده از این مدل‌های جدید به روز کرد. برای این منظور فایل Scripts\Routes\posts.js را گشوده و مدل آن‌را به نحو ذیل ویرایش کنید:
Blogger.PostsRoute = Ember.Route.extend({
    //controllerName: 'posts', // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //renderTemplare: function () {
    //    this.render('posts'); // مقدار پیش فرض است و نیازی به ذکر آن نیست
    //},
    model: function () {
        return this.store.find('post');
    }
});
در اینجا this.store معادل data store برنامه است که مطابق تنظیمات برنامه، همان ember data می‌باشد. سپس متد find را به همراه نام مدل، به صورت رشته‌ای در اینجا مشخص می‌کنیم.
به همین ترتیب فایل Scripts\Routes\recent-comments.js را نیز جهت استفاده از data store ویرایش خواهیم کرد:
Blogger.RecentCommentsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('comment');
    }
});
و فایل Scripts\Routes\post.js که در آن منطق یافتن یک مطلب بر اساس آدرس مختص به آن قرار دارد، به صورت ذیل بازنویسی می‌شود:
Blogger.PostRoute = Ember.Route.extend({
    model: function (params) {
        return this.store.find('post', params.post_id);
    }
});
اگر متد find بدون پارامتر ذکر شود، به معنای بازگشت تمامی عناصر موجود در آن مدل خواهد بود و اگر پارامتر دوم آن مانند این مثال تنظیم شود، تنها همان وهله‌ی درخواستی را بازگشت می‌دهد.


افزودن امکان ثبت یک مطلب جدید

تا اینجا اگر برنامه را اجرا کنید، برنامه بدون خطا بارگذاری خواهد شد اما فعلا رکوردی را برای نمایش ندارد. در ادامه، برنامه را جهت افزودن مطالب جدید توسعه خواهیم داد. برای اینکار ابتدا به فایل Scripts\App\router.js مراجعه کرده و سپس مسیریابی جدید new-post را تعریف خواهیم کرد:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' });
    this.resource('new-post');
});
اکنون در صفحه‌ی اول سایت، توسط قالب Scripts\Templates\posts.hbs، دکمه‌ای را جهت ایجاد یک مطلب جدید اضافه خواهیم کرد:
<h2>Ember.js blog</h2>
<ul>
    {{#each post in arrangedContent}}
    <li>{{#link-to 'post' post.id}}{{post.title}}{{/link-to}}</li>
    {{/each}}
</ul>
 
<a href="#" class="btn btn-primary" {{action 'sortByTitle' }}>Sort by title</a>
{{#link-to 'new-post' classNames="btn btn-success"}}New Post{{/link-to}}
در اینجا دکمه‌ی New Post به مسیریابی جدید new-post اشاره می‌کند.
برای تعریف عناصر نمایشی این مسیریابی، فایل جدید قالب Scripts\Templates\new-post.hbs را با محتوای زیر اضافه کنید:
<h1>New post</h1>
<form>
  <div class="form-group">
    <label for="title">Title</label>
    {{input value=title id="title" class="form-control"}}
  </div>

  <div class="form-group">
    <label for="body">Body</label>
    {{textarea value=body id="body" class="form-control" rows="5"}}
  </div>


  <button class="btn btn-primary" {{action 'save'}}>Save</button>
</form>
با نمونه‌ی این فرم در قسمت قبل در حین ویرایش یک مطلب، آشنا شدیم. دو المان دریافت اطلاعات در آن قرار دارند که هر کدام به خواص مدل برنامه bind شده‌اند. همچنین یک دکمه‌ی save، با اکشنی به همین نام در اینجا تعریف شده‌است.
پس از آن نیاز است نام فایل قالب new-post را به template loader برنامه در فایل index.html اضافه کرد:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post'
    ]);
</script>
برای مدیریت دکمه‌ی save این قالب جدید نیاز است کنترلر جدیدی را در فایل جدید Scripts\Controllers\new-post.js تعریف کنیم؛ با این محتوا:
Blogger.NewPostController = Ember.Controller.extend({
    actions: {
        save: function () {
            var newPost = this.store.createRecord('post', {
                title: this.get('title'),
                body: this.get('body')
            });
            newPost.save();
            this.transitionToRoute('posts');
        }
    }
});
به همراه افزودن مدخلی از آن به فایل index.html برنامه:
 <script src="Scripts/Controllers/new-post.js" type="text/javascript"></script>

در اینجا کنترلر جدید NewPostController را مشاهده می‌کنید. از این جهت که برای دسترسی به خواص مدل تغییر کرده، از متد this.get استفاده شده‌است، نیازی نیست حتما از یک ObjectController مانند قسمت قبل استفاده کرد و Controller معمولی نیز برای اینکار کافی است.
آرگومان اول this.store.createRecord نام مدل است و آرگومان دوم آن، وهله‌ای که قرار است به آن اضافه شود. همچنین باید دقت داشت که برای تنظیم یک خاصیت، از متد this.set و برای دریافت مقدار یک خاصیت تغییر کرده از this.get به همراه نام خاصیت مورد نظر استفاده می‌شود و نباید مستقیما برای مثال از this.title استفاده کرد.
this.store.createRecord صرفا یک شیء جدید (ember data object) را ایجاد می‌کند. برای ذخیره سازی نهایی آن باید متد save آن‌را فراخوانی کرد (پیاده سازی الگوی active record است). به این ترتیب این شیء در local storage ذخیره خواهد شد.
پس از ذخیره‌ی مطلب جدید، از متد this.transitionToRoute استفاده شده‌است. این متد، برنامه را به صورت خودکار به صفحه‌ی متناظر با مسیریابی posts هدایت می‌کند.

اکنون برنامه را اجرا کنید. بر روی دکمه‌ی سبز رنگ new post در صفحه‌ی اول کلیک کرده و یک مطلب جدید را تعریف کنید. بلافاصله عنوان و لینک متناظر با این مطلب را در صفحه‌ی اول سایت مشاهده خواهید کرد.
همچنین اگر برنامه را مجددا بارگذاری کنید، این مطالب هنوز قابل مشاهده هستند؛ زیرا در local storage مرورگر ذخیره شده‌اند.


در اینجا اگر به لینک‌های تولید شده دقت کنید، id آن‌ها عددی نیست. این روشی است که local storage با آن کار می‌کند.


افزودن امکان حذف یک مطلب به سایت

برای حذف یک مطلب، دکمه‌ی حذف را به انتهای قالب Scripts\Templates\post.hbs اضافه خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}


سپس کنترلر Scripts\Controllers\post.js را جهت مدیریت اکشن جدید delete به نحو ذیل تکمیل می‌کنیم:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();

            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                this.get('model').destroyRecord();
                this.transitionToRoute('posts');
            }
        }
    }
});
متد destroyRecord، مدل انتخابی را هم از حافظه و هم از data store حذف می‌کند. سپس کاربر را به صفحه‌ی اصلی سایت هدایت خواهیم کرد.
متد save نیز در اینجا بهبود یافته‌است. ابتدا مدل جاری دریافت شده و سپس متد save بر روی آن فراخوانی می‌شود. به این ترتیب اطلاعات از حافظه به local storage نیز منتقل خواهند شد.


ثبت و نمایش نظرات به همراه تنظیمات روابط اشیاء در Ember Data

در ادامه قصد داریم امکان افزودن نظرات را به مطالب، به همراه نمایش آن‌‌ها در ذیل هر مطلب، پیاده سازی کنیم. برای اینکار نیاز است رابطه‌ی بین یک مطلب و نظرات مرتبط با آن‌را در مدل ember data مشخص کنیم. به همین جهت فایل Scripts\Models\post.js را گشوده و تغییرات ذیل را به آن اعمال کنید:
Blogger.Post = DS.Model.extend({
  title: DS.attr(),
  body: DS.attr(),
  comments: DS.hasMany('comment', { async: true })
});
در اینجا خاصیت جدیدی به نام comments به مدل مطلب اضافه شده‌است و توسط آن می‌توان به تمامی نظرات یک مطلب دسترسی یافت؛ تعریف رابطه‌ی یک به چند، به کمک متد DS.hasMany که پارامتر اول آن نام مدل مرتبط است. تعریف async: true برای کار با local storage اجباری است و در نگارش‌های آتی ember data حالت پیش فرض خواهد بود.
همچنین نیاز است یک سر دیگر رابطه را نیز مشخص کرد. برای این منظور فایل Scripts\Models\comment.js را گشوده و به نحو ذیل تکمیل کنید:
Blogger.Comment = DS.Model.extend({
    text: DS.attr(),
    post: DS.belongsTo('post', { async: true })
});
در اینجا خاصیت جدید post به مدل نظر اضافه شده‌است و مقدار آن از طریق متد DS.belongsTo که مدل post را به یک نظر، مرتبط می‌کند، تامین خواهد شد. بنابراین در این حالت اگر به شیء comment مراجعه کنیم، خاصیت جدید post.id آن، به id مطلب متناظر اشاره می‌کند.

در ادامه نیاز است بتوان تعدادی نظر را ثبت کرد. به همین جهت با تعریف مسیریابی آن شروع می‌کنیم. این مسیریابی تعریف شده در فایل Scripts\App\router.js نیز باید تو در تو باشد؛ زیرا قسمت ثبت نظر (new-comment) دقیقا داخل همان صفحه‌ی نمایش یک مطلب ظاهر می‌شود:
Blogger.Router.map(function () {
    this.resource('posts', { path: '/' });
    this.resource('about');
    this.resource('contact', function () {
        this.resource('email');
        this.resource('phone');
    });
    this.resource('recent-comments');
    this.resource('post', { path: 'posts/:post_id' }, function () {
        this.resource('new-comment');
    });
    this.resource('new-post');
});
لینک آن‌را نیز به انتهای فایل Scripts\Templates\post.hbs اضافه می‌کنیم. از این جهت که این لینک به مدل جاری اشاره می‌کند، با استفاده از متغیر this، مدل جاری را به عنوان مدل مورد استفاده مشخص خواهیم کرد:
<h2>{{title}}</h2>
{{#if isEditing}}
<form>
    <div class="form-group">
        <label for="title">Title</label>
        {{input value=title id="title" class="form-control"}}
    </div>
    <div class="form-group">
        <label for="body">Body</label>
        {{textarea value=body id="body" class="form-control" rows="5"}}
    </div>
    <button class="btn btn-primary" {{action 'save' }}>Save</button>
</form>
{{else}}
<p>{{body}}</p>
<button class="btn btn-primary" {{action 'edit' }}>Edit</button>
<button class="btn btn-danger" {{action 'delete' }}>Delete</button>
{{/if}}
 
<h2>Comments</h2>
{{#each comment in comments}}
<p>
    {{comment.text}}
</p>
{{/each}}
 
<p>{{#link-to 'new-comment' this class="btn btn-success"}}New comment{{/link-to}}</p>
{{outlet}}
پس از تکمیل روابط مدل‌ها، قالب Scripts\Templates\post.hbs را جهت استفاده از این خواص به روز خواهیم کرد. در تغییرات جدید، قسمت <h2>Comments</h2> به انتهای صفحه اضافه شده‌است. سپس حلقه‌ای بر روی خاصیت جدید comments تشکیل شده و مقدار خاصیت text هر آیتم نمایش داده می‌شود.
در انتهای قالب نیز یک {{outlet}} اضافه شده‌است. کار آن نمایش قالب ارسال یک نظر جدید، پس از کلیک بر روی لینک New Comment می‌باشد. این قالب را با افزودن فایل Scripts\Templates\new-comment.hbs با محتوای ذیل ایجاد خواهیم کرد:
<h2>New comment</h2>

<form>
  <div class="form-group">
    <label for="text">Your thoughts:</label>
    {{textarea value=text id="text" class="form-control" rows="5"}}
  </div>

  <button class="btn btn-primary" {{action "save"}}>Add your comment</button>
</form>
سپس نام این قالب را به template loader فایل index.html نیز اضافه می‌کنیم؛ تا در ابتدای بارگذاری برنامه شناسایی شده و استفاده شود:
<script type="text/javascript">
    EmberHandlebarsLoader.loadTemplates([
       'posts', 'about', 'application', 'contact', 'email', 'phone',
       'recent-comments', 'post', 'new-post', 'new-comment'
    ]);
</script>
این قالب به خاصیت text یک comment متصل بوده و همچنین اکشن جدیدی به نام save دارد. بنابراین برای مدیریت اکشن save، نیاز به کنترلری متناظر خواهد بود. به همین جهت فایل جدید Scripts\Controllers\new-comment.js را با محتوای ذیل ایجاد کنید:
Blogger.NewCommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        save: function () {
            var comment = this.store.createRecord('comment', {
                text: this.get('text')
            });
            comment.save();
 
            var post = this.get('controllers.post.model');
            post.get('comments').pushObject(comment);
            post.save();
 
            this.transitionToRoute('post', post.id);
        }
    }
});
و مدخل تعریف آن‌را نیز به صفحه‌ی index.html اضافه می‌کنیم:
 <script src="Scripts/Controllers/new-comment.js" type="text/javascript"></script>

قسمت ذخیره سازی comment جدید با ذخیره سازی یک post جدید که پیشتر بررسی کردیم، تفاوتی ندارد. از متد this.store.createRecord جهت معرفی وهله‌ای جدید از comment استفاده و سپس متد save آن، برای ثبت نهایی فراخوانی شده‌است.
در ادامه باید این نظر جدید را به post متناظر با آن مرتبط کنیم. برای اینکار نیاز است تا به مدل کنترلر post دسترسی داشته باشیم. به همین جهت خاصیت needs را به تعاریف کنترلر جاری به همراه نام کنترلر مورد نیاز، اضافه کرده‌‌ایم. به این ترتیب می‌توان توسط متد this.get و پارامتر controllers.post.model در کنترلر NewComment به اطلاعات کنترلر post دسترسی یافت. سپس خاصیت comments شیء post جاری را یافته و مقدار آن‌را به comment جدیدی که ثبت کردیم، تنظیم می‌کنیم. در ادامه با فراخوانی متد save، کار تنظیم ارتباطات یک مطلب و نظرهای جدید آن به پایان می‌رسد.
در آخر با فراخوانی متد transitionToRoute به مطلبی که نظر جدیدی برای آن ارسال شده‌است باز می‌گردیم.


همانطور که در این تصویر نیز مشاهده می‌کنید، اطلاعات ذخیره شده در local storage را توسط افزونه‌ی Ember Inspector نیز می‌توان مشاهده کرد.


افزودن دکمه‌ی حذف به لیست نظرات ارسالی

برای افزودن دکمه‌ی حذف، به قالب Scripts\Templates\post.hbs مراجعه کرده و قسمتی را که لیست نظرات را نمایش می‌دهد، به نحو ذیل تکمیل می‌کنیم:
{{#each comment in comments}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
همچنین برای مدیریت اکشن جدید delete، کنترلر جدید comment را در فایل Scripts\Controllers\comment.js اضافه خواهیم کرد.
Blogger.CommentController = Ember.ObjectController.extend({
    needs: ['post'],
    actions: {
        delete: function () {
            if (confirm('Do you want to delete this comment?')) {
                var comment = this.get('model');
                comment.deleteRecord();
                comment.save();
 
                var post = this.get('controllers.post.model');
                post.get('comments').removeObject(comment);
                post.save(); 
            }
        }
    }
});
به همراه تعریف مدخل آن در فایل index.html :
 <script src="Scripts/Controllers/comment.js" type="text/javascript"></script>

در این حالت اگر برنامه را اجرا کنید، پیام «Do you want to delete this post» را مشاهده خواهید کرد بجای پیام «Do you want to delete this comment». علت اینجا است که قالب post به صورت پیش فرض به کنترلر post متصل است و نه کنترلر comment. برای رفع این مشکل تنها کافی است از itemController به نحو ذیل استفاده کنیم:
{{#each comment in comments  itemController="comment"}}
<p>
    {{comment.text}}
    <button class="btn btn-xs btn-danger" {{action 'delete' }}>delete</button>
</p>
{{/each}}
به این ترتیب اکشن delete به کنترلر comment ارسال خواهد شد و نه کنترلر پیش فرض post جاری.
در کنترلر Comment روش دیگری را برای حذف یک رکورد مشاهده می‌کنید. می‌توان ابتدا متد deleteRecord را بر روی مدل فراخوانی کرد و سپس آن‌را save نمود تا نهایی شود. همچنین در اینجا نیاز است نظر حذف شده را از سر دیگر رابطه نیز حذف کرد. روش دسترسی به post جاری در این حالت، همانند توضیحات NewCommentController است که پیشتر بحث شد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_04.zip
 
مطالب
فعال سازی عملیات CRUD در Kendo UI Grid
پیشنیاز بحث
- «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid»

Kendo UI Grid دارای امکانات ثبت، ویرایش و حذف توکاری است که در ادامه نحوه‌ی فعال سازی آن‌‌ها را بررسی خواهیم کرد. مثالی که در ادامه بررسی خواهد شد، در تکمیل مطلب «فرمت کردن اطلاعات نمایش داده شده به کمک Kendo UI Grid» است.



تنظیمات Data Source سمت کاربر

برای فعال سازی صفحه بندی سمت سرور، با قسمت read منبع داده Kendo UI پیشتر آشنا شده بودیم. جهت فعال سازی قسمت‌های ثبت اطلاعات جدید (create)، به روز رسانی رکوردهای موجود (update) و حذف ردیفی مشخص (destroy) نیاز است تعاریف قسمت‌های متناظر را که هر کدام به آدرس مشخصی در سمت سرور اشاره می‌کنند، اضافه کنیم:
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    create: {
                        url: "api/products",
                        contentType: 'application/json; charset=utf-8',
                        type: "POST"
                    },
                    update: {
                        url: function (product) {
                            return "api/products/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "PUT"
                    },
                    destroy: {
                        url: function (product) {
                            return "api/products/" + product.Id;
                        },
                        contentType: 'application/json; charset=utf-8',
                        type: "DELETE"
                    },
                    //...
                },
                schema: {
                    //...
                    model: {
                        id: "Id", // define the model of the data source. Required for validation and property types.
                        fields: {
                            "Id": { type: "number", editable: false }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string", validation: { required: true } },
                            "IsAvailable": { type: "boolean" },
                            "Price": { type: "number", validation: { required: true, min: 1 } },
                            "AddDate": { type: "date", validation: { required: true } }
                        }
                    }
                },
                batch: false, // enable batch editing - changes will be saved when the user clicks the "Save changes" button
                //...
            });
- همانطور که ملاحظه می‌کنید، حالت‌های update و destroy بر اساس Id ردیف انتخابی کار می‌کنند. این Id را باید در قسمت model مربوط به اسکیمای تعریف شده، دقیقا مشخص کرد. عدم تعریف فیلد id، سبب خواهد شد تا عملیات update نیز در حالت create تفسیر شود.
- به علاوه در اینجا به ازای هر فیلد، مباحث اعتبارسنجی نیز اضافه شده‌اند؛ برای مثال فیلدهای اجباری با required: true مشخص گردیده‌اند.
- اگر فیلدی نباید ویرایش شود (مانند فیلد Id)، خاصیت editable آن‌را false کنید.
- در data source امکان تعریف خاصیتی به نام batch نیز وجود دارد. حالت پیش فرض آن false است. به این معنا که در حالت ویرایش، تغییرات هر ردیفی، یک درخواست مجزا را به سمت سرور سبب خواهد شد. اگر آن‌را true کنید، تغییرات تمام ردیف‌ها در طی یک درخواست به سمت سرور ارسال می‌شوند. در این حالت باید به خاطر داشت که پارامترهای سمت سرور، از حالت یک شیء مشخص باید به لیستی از آن‌ها تغییر یابند.


مدیریت سمت سرور ثبت، ویرایش و حذف اطلاعات

در حالت ثبت، متد Post، توسط آدرس مشخص شده در قسمت create منبع داده گرید، فراخوانی می‌گردد:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Post(Product product)
        {
            if (!ModelState.IsValid)
                return Request.CreateResponse(HttpStatusCode.BadRequest);

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

            var response = Request.CreateResponse(HttpStatusCode.Created, product);
            response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = product.Id }));
            // گرید آی دی جدید را به این صورت دریافت می‌کند
            response.Content = new ObjectContent<DataSourceResult>(
                new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter());
            return response;
        }
    }
}
نکته‌ی مهمی که در اینجا باید به آن دقت داشت، نحوه‌ی بازگشت Id رکورد جدید ثبت شده‌است. در این مثال، قسمت schema منبع داده سمت کاربر به نحو ذیل تعریف شده‌است:
            var productsDataSource = new kendo.data.DataSource({
                //...
                schema: {
                    data: "Data",
                    total: "Total",
                }
                //...
            });
از این جهت که خروجی متد Get بازگرداننده‌ی اطلاعات صفحه بندی شده، از نوع DataSourceResult است و این نوع، دارای خواصی مانند Data، Total و Aggergate است:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public DataSourceResult Get(HttpRequestMessage requestMessage)
        {
            var request = JsonConvert.DeserializeObject<DataSourceRequest>(
                requestMessage.RequestUri.ParseQueryString().GetKey(0)
            );

            var list = ProductDataSource.LatestProducts;
            return list.AsQueryable()
                       .ToDataSourceResult(request.Take, request.Skip, request.Sort, request.Filter);
        }
    }
}
بنابراین در متد Post نیز باید بر این اساس، response.Content را از نوع لیستی از DataSourceResult تعریف کرد تا Kendo UI Grid بداند که Id رکورد جدید را باید از فیلد Data، همانند تنظیمات schema منبع داده خود، دریافت کند.
response.Content = new ObjectContent<DataSourceResult>(
                              new DataSourceResult { Data = new[] { product } }, new JsonMediaTypeFormatter());
اگر این تنظیم صورت نگیرد، Id رکورد جدید را در گرید، مساوی صفر مشاهده خواهید کرد و عملا بدون استفاده خواهد شد؛ زیرا قابلیت ویرایش و حذف خود را از دست می‌دهد.

متدهای حذف و به روز رسانی سمت سرور نیز چنین امضایی را خواهند داشت:
namespace KendoUI06.Controllers
{
    public class ProductsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = ProductDataSource.LatestProducts.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            ProductDataSource.LatestProducts.Remove(item);

            return Request.CreateResponse(HttpStatusCode.OK, item);
        }

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


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

            ProductDataSource.LatestProducts[item.Index] = product;
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }
}
حالت Update از HTTP Verb خاصی به نام Put استفاده می‌کند و ممکن است در این بین خطای The requested resource does not support http method 'PUT' را دریافت کنید. برای رفع آن ابتدا بررسی کنید که آیا Web.config برنامه دارای تعاریف ExtensionlessUrlHandler هست یا خیر. همچنین مزین کردن این متد با ویژگی HttpPut، مشکل را برطرف می‌کند.


تنظیمات Kendo UI Grid جهت فعال سازی CRUD

در ادامه کلیه تغییرات مورد نیاز جهت فعال سازی CRUD را در Kendo UI، به همراه مباحث بومی سازی عبارات متناظر با دکمه‌ها و صفحات خودکار مرتبط، مشاهده می‌کنید:
            $("#report-grid").kendoGrid({
                //....
                editable: {
                    confirmation: "آیا مایل به حذف ردیف انتخابی هستید؟",
                    destroy: true, // whether or not to delete item when button is clicked
                    mode: "popup", // options are "incell", "inline", and "popup"
                    //template: kendo.template($("#popupEditorTemplate").html()), // template to use for pop-up editing
                    update: true, // switch item to edit mode when clicked?
                    window: {
                        title: "مشخصات محصول"   // Localization for Edit in the popup window
                    }
                },
                columns: [
                //....
                    {
                        command: [
                            { name: "edit", text: "ویرایش" },
                            { name: "destroy", text: "حذف" }
                        ],
                        title: "&nbsp;", width: "160px"
                    }
                ],
                toolbar: [
                    { name: "create", text: "افزودن ردیف جدید" },
                    { name: "save", text: "ذخیره‌ی تمامی تغییرات" },
                    { name: "cancel", text: "لغو کلیه‌ی تغییرات" },
                    { template: kendo.template($("#toolbarTemplate").html()) }
                ],
                messages: {
                    editable: {
                        cancelDelete: "لغو",
                        confirmation: "آیا مایل به حذف این رکورد هستید؟",
                        confirmDelete: "حذف"
                    },
                    commands: {
                        create: "افزودن ردیف جدید",
                        cancel: "لغو کلیه‌ی تغییرات",
                        save: "ذخیره‌ی تمامی تغییرات",
                        destroy: "حذف",
                        edit: "ویرایش",
                        update: "ثبت",
                        canceledit: "لغو"
                    }
                }
            });
- ساده‌ترین حالت CRUD در Kendo UI با مقدار دهی خاصیت editable آن به true آغاز می‌شود. در این حالت، ویرایش درون سلولی یا incell فعال خواهد شد که مباحث batching ابتدای بحث، فقط در این حالت کار می‌کند. زمانیکه incell editing فعال است، کاربر می‌تواند تمام ردیف‌ها را ویرایش کرده و در آخر کار بر روی دکمه‌ی «ذخیره‌ی تمامی تغییرات» موجود در نوار ابزار، کلیک کند. در سایر حالات، هر بار تنها یک ردیف را می‌توان ویرایش کرد.
- برای فعال سازی تولید صفحات خودکار ویرایش و افزودن ردیف‌ها، نیاز است خاصیت editable را به نحوی که ملاحظه می‌کنید، مقدار دهی کرد. خاصیت mode آن سه حالت incell (پیش فرض)، inline و popup را پشتیبانی می‌کند.
- اگر حالت‌های inline و یا popup را فعال کردید، در انتهای ستون‌های تعریف شده، نیاز است ستون ویژه‌ای به نام command را مطابق تعاریف فوق، تعریف کنید. در این حالت دو دکمه‌ی ویرایش و ثبت، فعال می‌شوند و اطلاعات خود را از تنظیمات data source گرید دریافت می‌کنند. دکمه‌ی ویرایش در حالت incell کاربردی ندارد (چون در این حالت کاربر با کلیک درون یک سلول می‌تواند آن‌را مانند برنامه‌ی اکسل ویرایش کند). اما دکمه‌ی حذف در هر سه حالت قابل استفاده است.
- به نوار ابزار گرید، سه دکمه‌ی افزودن ردیف‌های جدید، ذخیره‌ی تمامی تغییرات و لغو تغییرات صورت گرفته، اضافه شده‌اند. این دکمه‌ها استاندارد بوده و در اینجا نحوه‌ی بومی سازی پیام‌های مرتبط را نیز مشاهده می‌کنید. همانطور که عنوان شد، دکمه‌های «تمامی تغییرات» در حالت فعال سازی batching در منبع داده و استفاده از incell editing معنا پیدا می‌کند. در سایر حالات این دو دکمه کاربردی ندارند. اما دکمه‌ی افزودن ردیف‌های جدید در هر سه حالت کاربرد دارد و یکسان است.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
KendoUI06.zip
مطالب
خواندن فید گزارش آب و هوای یاهو با استفاده از روش Xml serialization

در مطلب قبلی (در مورد کتابخانه anti-xss مایکروسافت) از روش xml serialization برای خواندن فایل xml حملات استفاده کردیم.
ایجاد این کلاس و نگاشت اشیاء با توجه به ساختار ساده آن به صورت دستی و به‌سادگی انجام شد. اکنون به مثال زیر دقت بفرمائید:
سرویس آب و هوای یاهو برای شهرهای مختلف ایران از طریق لینک زیر قابل استفاده است:
http://weather.yahoo.com/regional/IRXX.html
اگر به صفحات شهرهای مختلف مراجعه نمائید، یک فید rss هم مشاهده خواهید کرد، برای مثال در مورد تهران داریم:
http://weather.yahooapis.com/forecastrss?p=IRXX0018&u=c
ساختار این فایل xml تا حدودی با یک rss استاندارد تطابق دارد. اما اگر به سورس xml آن دقت کنیم تگ‌های دیگری را نیز مشاهده خواهیم کرد که برای مثال دما ، تاریخ و شرایط جوی را به صورت دقیقی و با استفاده از اصول xml ارائه می‌دهند.

<yweather:condition text="Partly Cloudy" code="29" temp="10" date="Tue, 11 Nov 2008 5:30 pm IRT" />

خوب، برای دریافت این اطلاعات چه باید کرد؟ یکی از روش‌های متداول برای کار با این نوع داده‌ها، استفاده از کلاس DataSet در دات نت و فراخوانی متد ReadXml آن است (یک آدرس اینترنتی را هم می‌تواند دریافت کند). سپس مطابق روش‌های معمول ADO.Net می‌توان به تگ‌ها ومقادیر آنها دسترسی داشت.
روش‌ بالا هر چند مشکلی ندارد اما به زیبایی کار با خواص یک کلاس متناظر با آن فایل xml نیست. اما در اینجا برای استفاده از روش xml serialization یک مشکل وجود دارد! ایجاد دستی این کلاس که بیانگر عملکرد آن فایل xml است کار ساده‌ای نیست.
خوشبختانه به همراه SDK‌ دات نت فریم ورک 2، برنامه‌ای به نام xsd.exe نیز همراه است که کار ایجاد یک کلاس cs یا vb را از یک فایل xml جهت این منظور انجام می‌دهد (این برنامه برای مثال در مسیر C:\Program Files\Microsoft.NET\SDK\v2.0\Bin قرار دارد).

برای ایجاد فایل کلاس به صورت خودکار از روی یک فایل xml موجود باید به ترتیب زیر عمل کرد:
الف) ایجاد فایل xsd متناظر (XML Schema Definition)
برای اینکار در خط فرمان تایپ کنید:
xsd.exe file.xml

نکته 1:
روش دیگر انجام این کار : فایل xml را در VS.net باز کنید، از منوی بالای صفحه گزینه xml را انتخاب نموده و بر روی دکمه Create Schema کلیک کنید.

ب) ایجاد فایل cs یا vb از روی فایل(های) xsd ایجاد شده
در اینجا برای فید آب و هوای یاهو سه فایل xsd تولید خواهد شد. برای تبدیل آنها به کلاس cs باید دستور زیر را در خط فرمان اجرا کرد:

Xsd.exe file_1.xsd file_2.xsd file_3.xsd /c

این مورد نکته مهمی است و تنها اگر یکی از فایل‌ها اینجا ذکر شوند، کلاس ناقصی تشکیل خواهد شد. (برای نمونه فایل xssAttacks.xml مطلب قبلی، ساختار ساده‌ای داشته و تنها به یک فایل xsd ختم خواهد شد)

نکته 2:
برای انتخاب زبان VB (با توجه به این‌که پیش فرض آن CS است) می‌توان به صورت زیر عمل کرد:
xsd.exe file.xsd /c  /l:vb

نکته 3:
برای تولید فایل xsd ، از برنامه Infer.exe نیز می‌توان استفاده کرد (خروجی نهایی دقیق‌تری را ارائه می‌دهد). این برنامه را از اینجا دریافت کنید.

تصاویر زیر مقایسه دو فایل کلاس نهایی تولید شده از xsd های این دو برنامه است:






پس از طی این مراحل فایل کلاس ما برای xml serialization آماده خواهد شد. مرحله بعد دریافت اطلاعات و نگاشت آن به این کلاس تولید شده است:

public static rss DeserializeFromXML()
{
XmlSerializer deserializer =
new XmlSerializer(typeof(rss));
using (XmlReader reader = XmlReader.Create("http://weather.yahooapis.com/forecastrss?p=IRXX0018&u=c"))
{
return (rss)deserializer.Deserialize(reader);
}
}

کلاس rss از فید xml و فایل‌های xsd آن که تولید کردیم به صورت خودکار ایجاد شده است.
اکنون برای مثال خواندن وضعیت فعلی جوی از فید دریافتی به سادگی زیر است:

rss data = DeserializeFromXML();
MessageBox.Show(data.channel.item.condition.text);


مطالب
مبانی TypeScript؛ متدها
توابع جزو ساختارهای پایه‌ایی زبان جاوا اسکریپت هستند و از آنها جهت شبیه‌سازی کلاس‌ها، کپسوله‌سازی اطلاعات و همچنین ایجاد ماژول‌ها استفاده می‌شود. هر چند در زبان TypeScript به راحتی می‌توانیم از کلاس‌ها، فضاهای نام و ماژول‌ها استفاده کنیم، اما هنوز هم توابع، نقشی کلیدی را در انجام کارها ایفا می‌کنند. نکته‌ی قابل توجه این است که زبان TypeScript یکسری قابلیت‌های اضافه‌تری را به توابع استاندارد جاوا اسکریپت اضافه کرده است که در ادامه آنها را بررسی خواهیم کرد.

نحوه‌ی ایجاد توابع
همانند جاوا اسکریپت، در زبان TypeScript نیز می‌توانیم توابع را هم به صورت named function و  هم به صورت anonymous function ایجاد کنیم. در کدهای زیر نحوه‌ی تعریف هر دو نوع نشان داده شده است:
// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x+y; };
در زبان TypeScript نیز می‌توانیم به متغیرهای تعریف شده‌ی در خارج بدنه‌ی تابع، دسترسی داشته باشیم. در این‌حالت خواهیم گفت که متغیرها توسط تابع capture شده‌اند:
let z = 100;

function addToZ(x, y) {
    return x + y + z;
}

تعیین نوع (Type) برای توابع
همانطور که قبلاً عنوان شد، یکی از مزایای زبان TypeScript، امکان معرفی نوع‌ها است. یعنی با کمک تعیین نوع می‌توانیم کدهای نهایی نوشته شده را امن‌تر کنیم و همچنین در زمان استفاده، Intellisense با وجود نوع‌ها، پیشنهادهای بهتر و دقیق‌تری را ارائه می‌دهد. جهت درک بهتر موضوع اجازه دهید برای توابعی که در مثال قبلی مطرح شدند، یکسری نوع را مشخص کنیم:
function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x+y; };
همانطور که مشاهده می‌کنید، توانسته‌ایم هم برای پارامترها و هم برای خروجی تابع، نوع‌هایی را مشخص کنیم. مزیت آن این است که پیش از اینکه کدهای شما در زمان اجرا به خطا بر بخورند، در زمان کامپایل، مشکلات موجود توسط کامپایلر، گوشزد می‌شوند. به عنوان مثال اگر در زمان توسعه، برای یکی از پارامترهای تابع add، مقداری رشته‌ایی را ارسال کنیم، کامپایلر به شما هشدار لازم را خواهد داد:


Function Types
در TypeScript علاوه بر امکان تعیین نوع، برای پارامتر و همچنین نوع بازگشتی تابع، می‌توانیم خود تابع را نیز به عنوان یک نوع تعریف کنیم. function types با تعیین نوع برای پارمتر دریافتی و همچنین تعیین نوع بازگشتی تابع تعریف می‌شوند. به عنوان مثال تابع زیر، یک پارامتر از نوع number را دریافت کرده و در نهایت یک رشته را در خروجی بر می‌گرداند:
function PublicationMessage(year: number): string {
    return 'Date published: ' + year;
}
اکنون می‌توانیم یک متغیر ایجاد کنیم که ارجاعی را به تابع فوق داشته باشد:
let publishFunc: (someYear: number) => string;
با استفاده از سینتکس فوق، توانسته‌ایم یک function type را تعریف کنیم. در کد فوق از کلمه‌ی کلیدی let و همچنین علامت دو نقطه بعد از نام متغیر استفاده کرده‌ایم. سپس پارامترها و همچنین انواع آنها را درون پرانتز تعیین کرده‌ایم و در نهایت بعد از علامت <=، نوع بازگشتی تابع را تعیین کرده‌ایم. در واقع توسط کد فوق، یک امضاء را برای توابعی که قرار است به این متغیر انتساب داده شوند، تعریف کرده‌ایم. اکنون که متغیر را تعریف کردیم و همچنین یک نوع را برای آن مشخص کردیم، می‌توانیم تابعی را که دارای این امضاء است، به آن انتساب دهیم:
publishFunc = PublicationMessage;

let message: string = publishFunc(2016);
در واقع اکنون به متغیر publishFunc، تنها تابعی را می‌توانیم انتساب دهیم که یک پارامتر از جنس number را از ورودی دریافت کرده و همچنین یک رشته را به عنوان خروجی برگرداند. در نتیجه اگر تابعی غیر از امضاء تعیین شده را به متغیر publishFunc انتساب دهیم، کامپایلر TypeScript به ما هشدار خواهد داد:

همچنین می‌توان function type را به صورت inline نیز تعریف کرد:

let myAdd: (baseValue:number, increment:number) => number =
    function(x, y) { return x + y; };


Optional and Default Parameters 

در جاوا اسکریپت تمامی پارامترهای یک تابع اختیاری هستند. اما TypeScript کمی متفاوت است. یعنی در حالت پیش‌فرض، ذکر تمامی پارامترها ضروری است؛ مگر اینکه پارامترهای موردنیاز را به صورت اختیاری تعیین کنید. به طور مثال در تابع زیر دو پارامتر را تعریف کرده‌ایم:

function CreateCustomer(name: string, age?: number) {}

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

برای تعیین مقدار پیش‌فرض برای هر پارامتر نیز می‌توانیم به این‌صورت عمل کنیم:

function GetBookByTitle(title: string = 'C# 6.0 in a Nutshell') {}

default parameters در صورتیکه بعد از required parameters آورده شوند، به عنوان optional در نظر گرفته می‌شوند. یعنی در این‌حالت لزومی به گذاشتن علامت سوال، بعد از نام پارامتر نیست. نکته‌ی قابل توجه‌ایی که در استفاده از default parameters وجود دارد این است که علاوه بر رشته‌ها می‌توان عبارات (expressions) را نیز به آنها اختصاص داد:

function GetBookByTitle(title: string = GetMostPopularBooks()) {}


Rest Parameters

rest parameters به شما این امکان را می‌دهند تا به تعداد نامحدودی پارامتر به یک تابع ارسال کنید:

function GetBooksReadForCust(name: string, ...bookIDs: number[]) {}

تابع فوق دو پارامتر را از ورودی دریافت می‌کند. پارامتر دوم این تابع به صورت rest تعریف شده است. یعنی برای پارامتر دوم می‌توانیم هر تعداد پارامتری را به این تابع ارسال کنیم. همچنین برای نوع این پارامتر، یک آرایه از نوع number را تعیین کرده‌ایم. یعنی پارامترهای دریافتی، درون یک آرایه از نوع number ذخیره خواهند شد. در ES 5 برای داشتن این چنین قابلیتی از شیء  arguments استفاده می‌کردیم. یعنی تابع فوق را می‌بایستی اینگونه می‌نوشتیم:

function GetBooksReadForCust(name) {
    var bookIDs = [];
    for (var _i = 1; _i < arguments.length; _i++) {
        bookIDs[_i - 1] = arguments[_i];
    }
}


استفاده از this

درک this در جاوا اسکریپت، در ابتدا باعث مقداری سردرگمی می‌شود. یعنی مقدار آن در زمان فراخوانی تابع، ست خواهد شد. یعنی در هر بلاک از کد، وضعیت‌های متفاوتی را ارائه می‌دهد. به عنوان مثال درون callback مربوط به تابع setInterval در تابع زیر می‌خواهیم به مقدار متغیر publishDate دسترسی داشته باشم:

function Book() {
    let self = this;
    self.publishDate = 2016;
    setInterval(function() {
        console.log(self.publishDate);
    }, 1000);
}

همانطور که مشاهده می‌کنید برای دسترسی به این پراپرتی، مقدار this را درون یک متغیر با نام self، در ابتدا تعریف کرده‌ایم. زیرا استفاده‌ی مستقیم از this.publishDate درون callback به چیز دیگری اشاره می‌کند. این روش در ES 5 خیلی رایج است. اما با استفاده از Arrow Functions به راحتی می‌توانیم به this در هر جایی دسترسی داشته باشیم. بنابراین کد فوق را می‌توانیم به این صورت بازنویسی کنیم:

function Book() {
    this.publishDate = 2016;
    setInterval(() => {
        console.log(this.publishDate);
    }, 1000);
}

در واقع Arrow Function در پشت صحنه کار capture کردن this را برایمان انجام خواهد داد.


Function overloads

قابلیت function overloading در بیشتر typed languageها در دسترس می‌باشد. همانطور که می‌دانید این قابلیت جهت تعریف امضاءهای مختلف برای یک تابع استفاده می‌شود. یعنی ایجاد توابعی با یک نام، اما با انواع متفاوت. از آنجائیکه TypeScript به جاوا اسکریپت کامپایل می‌شود، در نتیجه جاوا اسکریپت فاقد نوع (type) است. پس در زمان کامپایل نوع‌ها برداشته خواهند شد. بنابراین داشتن توابعی همنام باعث بروز مشکلاتی خواهد شد. برای داشتن نسخه‌های مختلفی از یک تابع می‌توانیم تعاریف موردنیازمان را ارائه داده، اما تنها یک پیاده‌سازی داشته باشیم. برای مثال می‌خواهیم یک overload دیگر برای تابع زیر داشته باشیم:

function GetTitles(author: string) : string[];

تابع فوق یک رشته را از ورودی دریافت کرده و در نهایت یک آرایه از رشته‌ها را بر می‌گرداند. برای overload دیگر این تابع می‌خواهیم به جای دریافت رشته، یک boolean از ورودی دریافت کنیم:

function GetTitles(available: boolean) : string[];

همانطور که مشاهده می‌کنید، هیچکدام از overloadهای فوق پیاده‌سازی‌ایی ندارند. در واقع تا اینجا به TypeScript گفته‌ایم که نیاز به دو نسخه از تابع GetTitles خواهیم داشت. اکنون می‌توانیم یک پیاده‌سازی کلی برای دو overload فوق داشته باشیم:

function GetTitles(bookProperty: any) : string[] {
    if(typeof bookProperty == 'string') {
        // some code
    } else if (typeof bookProperty == 'boolean') {
        // some code
    }
    
    return result;
}

همانطور که عنوان شد، تنها پیاده‌سازی فوق را برای تمامی overloadها خواهیم داشت. در نتیجه اینبار نوع پارامتر ورودی را any تعریف کرده‌ایم. سپس درون بدنه‌ی تابع، نوع پراپرتی را توسط typeof تشخیص داده‌ایم. بنابراین برای فراخوانی هر یک از overloadها، می‌توانیم کدهای خاصی را اجرا کنیم.

نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
تابع createAccessTokenAsync در کلاس TokenStoreService  زمانی که از Claim دارید یک شی ایجاد می‌کنید ، تاریخ را به ToUnixEpochDate تبدیل می‌کنید. علت این تبدیل چیست ؟
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToUnixEpochDate().ToString(), ClaimValueTypes.Integer64),

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


مدل برنامه، جهت تامین داده‌های خود ارجاع دهنده و درختی

فرض کنید قصد داریم لیستی از کامنت‌های تو در تو را مدل سازی کنیم که در آن هر کامنت، می‌تواند چندین کامنت تا بی‌نهایت سطح تو در تو را داشته باشد:
namespace BlazorTreeView.ViewModels;

public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
برای نمونه بر اساس این مدل، منبع داده‌ی فرضی زیر را تهیه می‌کنیم:
using BlazorTreeView.ViewModels;

namespace BlazorTreeView.Pages;

public partial class TreeView
{
    private IReadOnlyDictionary<string, object> ChildrenHtmlAttributes { get; } =
        new Dictionary<string, object>(StringComparer.Ordinal)
        {
            { "style", "list-style: none;" },
        };

    private IList<Comment> Comments { get; } =
        new List<Comment>
        {
            new()
            {
                Text = "پاسخ یک",
            },
            new()
            {
                Text = "پاسخ دو",
                Comments =
                    new List<Comment>
                    {
                        new()
                        {
                            Text = "پاسخ اول به پاسخ دو",
                            Comments =
                                new List<Comment>
                                {
                                    new()
                                    {
                                        Text = "پاسخی به پاسخ اول پاسخ دو",
                                    },
                                },
                        },
                        new()
                        {
                            Text = "پاسخ دوم به پاسخ دو",
                        },
                    },
            },
            new()
            {
                Text = "پاسخ سوم",
            },
        };
}
این قطعه کد partial class که مربوط به فایل TreeView.razor.cs برنامه‌است، در حقیقت کدهای پشت صحنه‌ی کامپوننت مثال TreeView.razor است که در ادامه آن‌را توسعه خواهیم داد. در نهایت قرار است بتوانیم آن‌را به صورت زیر رندر کنیم:



طراحی کامپوننت DntTreeView

برای اینکه بتوانیم به یک کامپوننت با قابلیت استفاده‌ی مجدد بررسیم، کدهای نمایش اطلاعات تو در تو و درختی را توسط کامپوننت سفارشی DntTreeView پیاده سازی خواهیم کرد. پیشنیازهای آن نیز به صورت زیر است:
- این کامپوننت باید جنریک باشد؛ یعنی باید به صورت زیر شروع شود:
/// <summary>
///   A custom DntTreeView
/// </summary>
public partial class DntTreeView<TRecord>
{
چون باید بتوان یک لیست جنریک <IEnumerable<TRecord را به آن، جهت رندر ارسال کرد و قرار نیست این کامپوننت، تنها به شیء سفارشی Comment مثال جاری ما وابسته باشد. بنابراین اولین خاصیت آن، شیء جنریک Items است که لیست کامنت‌ها/عناصر را دریافت می‌کند:
    /// <summary>
    ///     The treeview's self-referencing items
    /// </summary>
    [Parameter]
    public IEnumerable<TRecord>? Items { set; get; }
- هنگام رندر هر آیتم کامنت باید بتوان یک قالب سفارشی را از کاربر دریافت کرد. نمی‌خواهیم صرفا برای مثال Text شیء Comment فوق را به صورت متنی و ساده نمایش دهیم. می‌خواهیم در حین رندر، کل شیء TRecord جاری را به مصرف کننده ارسال و یک قالب سفارشی را از آن دریافت کنیم. یعنی باید یک RenderFragment جنریک را به صورت زیر نیز داشته باشیم تا مصرف کننده بتواند TRecord در حال رندر را دریافت و قالب Htmlای خودش را بازگشت دهد:
    /// <summary>
    ///     The treeview item's template
    /// </summary>
    [Parameter]
    public RenderFragment<TRecord>? ItemTemplate { set; get; }
- همچنین همیشه باید به فکر عدم وجود اطلاعاتی برای نمایش نیز بود. به همین جهت بهتر است قالب دیگری را نیز از مصرف کننده برای اینکار درخواست کنیم و نحوه‌ی رندر سفارشی این قسمت را نیز به مصرف کننده واگذار کنیم:
    /// <summary>
    ///     The content displayed if the list is empty
    /// </summary>
    [Parameter]
    public RenderFragment? EmptyContentTemplate { set; get; }
- زمانیکه با شیء از پیش تعریف شده‌ی Comment این مثال کار می‌کنیم، کاملا مشخص است که خاصیت Comments آن تو در تو است:
public class Comment
{
    public IList<Comment> Comments = new List<Comment>();
    public string? Text { set; get; }
}
اما زمانیکه با یک کامپوننت جنریک کار می‌کنیم، نیاز است از مصرف کننده، نام این خاصیت تو در تو را به نحو واضحی دریافت کنیم؛ به صورت زیر:
    /// <summary>
    ///     The property which returns the children items
    /// </summary>
    [Parameter]
    public Expression<Func<TRecord, IEnumerable<TRecord>>>? ChildrenSelector { set; get; }
دلیل استفاده از Expression Funcها را در مطلب «static reflection» می‌توانید مطالعه کنید. زمانیکه قرار است از کامپوننت DntTreeView استفاده کنیم، ابتدا نوع جنریک آن‌را مشخص می‌کنیم، سپس لیست اشیاء ارسالی به آن‌را و در ادامه با استفاده از ChildrenSelector به صورت زیر، مشخص می‌کنیم که خاصیت Comments است که به همراه Children می‌باشد و تو در تو است:
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
و مرسوم است جهت بالابردن کارآیی Expression Funcها، آن‌ها را کامپایل و کش کنیم که نمونه‌ای از روش آن‌را به صورت زیر مشاهده می‌کنید:
public partial class DntTreeView<TRecord>
{
    private Expression? _lastCompiledExpression;
    internal Func<TRecord, IEnumerable<TRecord>>? CompiledChildrenSelector { private set; get; }

    // ...

    protected override void OnParametersSet()
    {
        if (_lastCompiledExpression != ChildrenSelector)
        {
            CompiledChildrenSelector = ChildrenSelector?.Compile();
            _lastCompiledExpression = ChildrenSelector;
        }
    }
}
تا اینجا ساختار کدهای پشت صحنه‌ی DntTreeView.razor.cs مشخص شد. اکنون UI این کامپوننت را به صورت زیر تکمیل می‌کنیم:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

@if (Items is null || !Items.Any())
{
    @EmptyContentTemplate
}
else
{
    <CascadingValue Value="this">
        <ul @attributes="AdditionalAttributes">
            @foreach (var item in Items)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    </CascadingValue>
}
در ابتدای کار، اگر آیتمی برای نمایش وجود نداشته باشد، EmptyContentTemplate دریافتی از استفاده کننده را رندر می‌کنیم. در غیراینصورت، حلقه‌ای را بر روی لیست Items ایجاد کرده و آن‌‌ها را یکی نمایش می‌دهیم. این نمایش، نکات زیر را به همراه دارد:
- نمایش توسط کامپوننت دومی به نام DntTreeViewChildrenItem انجام می‌شود که آن‌‌هم جنریک است و شیء item جاری را توسط خاصیت ParentItem دریافت می‌کند.
- در اینجا یک CascadingValue اشاره کننده به شیء this را هم مشاهده می‌کنید. این روش، یکی از روش‌های اجازه دادن دسترسی به خواص و امکانات یک کامپوننت والد، در کامپوننت‌های فرزند است که در ادامه از آن استفاده خواهیم کرد.


تکمیل کامپوننت بازگشتی DntTreeViewChildrenItem.razor

اگر به حلقه‌ی foreach (var item in Items) در کامپوننت DntTreeView.razor دقت کنید، یک سطح را بیشتر پوشش نمی‌دهد؛ اما کامنت‌های ما چندسطحی و تو در تو هستند و عمق آن‌ها هم مشخص نیست. به همین جهت نیاز است به نحوی بتوان یک طراحی recursive و بازگشتی را در کامپوننت‌های Blazor داشت که خوشبختانه این مورد پیش‌بینی شده‌است و هر کامپوننت Blazor، می‌تواند خودش را نیز فراخوانی کند:
@namespace BlazorTreeView.Pages.Components
@typeparam TRecord

<li @attributes="@SafeOwnerTreeView.ChildrenHtmlAttributes" @key="ParentItem?.GetHashCode()">
    @if (SafeOwnerTreeView.ItemTemplate is not null && ParentItem is not null)
    {
        @SafeOwnerTreeView.ItemTemplate(ParentItem)
    }
    @if (Children is not null)
    {
        <ul>
            @foreach (var item in Children)
            {
                <DntTreeViewChildrenItem TRecord="TRecord" ParentItem="item"/>
            }
        </ul>
    }
</li>
این‌ها کدهای DntTreeViewChildrenItem.razor هستند که در آن، ابتدا ItemTemplate دریافتی از والد یا همان DntTreeView.razor رندر می‌شود. سپس به کمک CompiledChildrenSelector ای که عنوان شد، یک شیء Children را تشکیل داده و آن‌را به خودش (فراخوانی مجدد DntTreeViewChildrenItem در اینجا)، ارسال می‌کند. این فراخوانی بازگشتی، سبب رندر تمام سطوح تو در توی شیء جاری می‌شود.

کدهای پشت صحنه‌ی این کامپوننت یعنی فایل DntTreeViewChildrenItem.razor.cs به صورت زیر است:
/// <summary>
///     A custom DntTreeView
/// </summary>
public partial class DntTreeViewChildrenItem<TRecord>
{
    /// <summary>
    ///     Defines the owner of this component.
    /// </summary>
    [CascadingParameter]
    public DntTreeView<TRecord>? OwnerTreeView { get; set; }

    private DntTreeView<TRecord> SafeOwnerTreeView =>
        OwnerTreeView ??
        throw new InvalidOperationException("`DntTreeViewChildrenItem` should be placed inside of a `DntTreeView`.");

    /// <summary>
    ///     Nested parent item to display
    /// </summary>
    [Parameter]
    public TRecord? ParentItem { set; get; }

    private IEnumerable<TRecord>? Children =>
        ParentItem is null || SafeOwnerTreeView.CompiledChildrenSelector is null
            ? null
            : SafeOwnerTreeView.CompiledChildrenSelector(ParentItem);
}
با استفاده از یک پارامتر از نوع CascadingParameter، می‌توان به اطلاعات شیء CascadingValue ای که در کامپوننت والد DntTreeView.razor قرا دادیم، دسترسی پیدا کنیم. سپس یکبار هم بررسی می‌کنیم که آیا نال هست یا خیر. یعنی قرار نیست که این کامپوننت فرزند، درجائی به صورت مستقیم استفاده شود. فقط قرار است داخل کامپوننت والد فراخوانی شود. به همین جهت اگر این CascadingParameter نال بود، یعنی این کامپوننت فرزند، به اشتباه فراخوانی شده و با صدور استثنائی این مساله را گوشزد می‌کنیم. اکنون که به SafeOwnerTreeView یا همان نمونه‌ای از شیء والد دسترسی پیدا کردیم، می‌توانیم پارامتر CompiledChildrenSelector آن‌را نیز فراخوانی کرده و توسط آن، به شیء تو در توی جدیدی در صورت وجود، جهت رندر بازگشتی آن رسید.
یعنی این کامپوننت ابتدا ParentItem، یا اولین سطح ممکن و در دسترس را رندر می‌کند. سپس با استفاده از Expression Func مهیای در کامپوننت والد، شیء فرزند را در صورت وجود یافته و سپس به صورت بازگشتی آن‌را با فراخوانی مجدد خودش ، رندر می‌کند.


روش استفاده از کامپوننت DntTreeView

اکنون که کار توسعه‌ی کامپوننت جنریک DntTreeView پایان یافت، روش استفاده‌ی از آن به صورت زیر است:
<div class="card" dir="rtl">
    <div class="card-header">
        DntTreeView
    </div>
    <div class="card-body">
        <DntTreeView
            TRecord="Comment"
            Items="Comments"
            ChildrenSelector="m => m.Comments"
            style="list-style: none;"
            ChildrenHtmlAttributes="ChildrenHtmlAttributes">
            <ItemTemplate Context="record">
                <div class="card mb-1">
                    <div class="card-body">
                        <span>@record.Text</span>
                    </div>
                </div>
            </ItemTemplate>
            <EmptyContentTemplate>
                <div class="alert alert-warning">
                    There is no item to display!
                </div>
            </EmptyContentTemplate>
        </DntTreeView>
    </div>
</div>
همانطور که مشاهده می‌کنید، چون کامپوننت جنریک است، باید نوع TRecord را که در مثال ما، شیء Comment است، مشخص کرد. سپس لیست نظرات، خاصیت تو در تو، قالب سفارشی نمایش Text نظرات (با توجه به Context دریافتی که امکان دسترسی به شیء جاری در حال رندر را میسر می‌کند) و همچنین قالب سفارشی نبود اطلاعاتی برای نمایش را تعریف می‌کنیم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorTreeView.zip
کامپوننت توسعه یافته‌ی در اینجا در هر دو حالت Blazor WASM و Blazor Server کار می‌کند.
مطالب
اصول طراحی شیء گرا: OO Design Principles

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

این مطالب نیز چکیده ای از آموزش‌های Lynda, Pluralsight , SkillFeed  و کتاب های Gang of four, Headfirst Design patterns  و ... میباشد


اصل اول: Encapsulate what varies

"آنچه را که تغییر می‌کند مشخص و جدا کن یا به عبارتی آنرا کپسوله کن"

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


اصل دوم: Program to an interface not implementation

" برنامه نویسی را متمرکز بر واسط کن، نه نحوه‌ی پیاده سازی "

برای این اصل تعابیر مختلفی ارائه شده است:

  • تمرکز بر واسط‌ها به معنای تمرکز بر رفتار است که باعث می‌شود انعطاف برنامه بیشتر شده و با تغییر نحوه‌ی پیاده سازی بتوان همچنان سیستمی پایدار داشت.
  • تمرکز بر "چه کاری انجام می‌شود" باعث می‌شود بدون نگرانی از "چگونگی انجام آن" بتوانیم معماری سیستم را طراحی کنیم.
  • واسط‌ها نقش پروتکل را دارند و باعث پنهان شدن نحوه‌ی پیاده سازی از چشم کلاینت (استفاده کننده‌ی خدمت) می‌شوند و آنها را ملزم میکنند تا به ورودی و خروجی تمرکز کنند.

برای رعایت این اصل باید:

  • سعی بر تعریف واسط برای اکثر کلاس‌ها کنیم
  • اشیا را از نوع واسط تعریف کنیم، نه کلاس‌های پیاده ساز آن

در کد زیر نحوه‌ی تعریف واسط را برای کلاس و تعریف اشیاء از نوع واسط را میبینیم: 

    public interface IMyInterface
    {
        void DoWork();
    }

    public class MyImplementation1 : IMyInterface
    {
        public void DoWork()
        {
            var implementationName = this.GetType().ToString();
            Console.WriteLine("DoWork from " + implementationName);
        }
    }
    public class MyImplementation2 : IMyInterface
    {
        public void DoWork()
        {
            var implementationName = this.GetType().ToString();
            Console.WriteLine("DoWork from " + implementationName);
        }
    }

    public class Context
    {
        IMyInterface myInterface;

        public void Print() 
        {
            myInterface = new MyImplementation1();
            myInterface.DoWork();

            myInterface = new MyImplementation2();
            myInterface.DoWork();
        }
    }


اصل سوم: Favor composition over inheritance

"ترکیب را بر توارث ترجیح بده"

رابطه "دارد" (has a ) انعطاف بیشتری نسبت به ارث بری یا "از نوع ... هست" ( is a ) دارد. برای مثال فرض کنید کلاس Enemy رفتار Movable را دارد و این رفتار در طول بازی بر حسب موقعیتی تغییر میکند. اگر این رفتار را با توارث مدل کنیم، یعنی Enemy از نوع Movable باشد، آنگاه برای هر نوع رفتار Movable (هر نوع پیاده سازی) باید یک نوع Enemy داشته باشیم و تصور کنید بعضی از این پیاده سازی‌ها در کلاس Player یکسان باشد. آنگاه کد باید دوباره تکرار شود. حال تصور کنید این موقعیت را با ترکیب مدل کنیم. آنگاه کلاس Enemy یک شیء از جنس Movable خواهد داشت و در زمان نیاز میتواند نوع این رفتار را با نمونه گیری از کلاس‌های پیاده ساز آن، تغییر دهد. در واقع با اینکار اصل اول را رعایت کرده ایم و بخش ثابت را از بخش متغیر جدا نموده ایم.

در زیر مدلسازی با توارث را میبینیم: 

public interface Movable
    {
        void Move();
    }
    public class EnemyBase : Movable
    {
        // Enemy properties goes here
        protected int x, y;
        public virtual void Move()
        {
            x += 2;
            y += 2;
        }
    }
    public class EnemyWithMoveType2 : EnemyBase
    {
        // override the move method
        public override void Move()
        {
            x += 4;
            y += 4;
        }
    }
    public class EnemyWithMoveType3 : EnemyBase
    {
        // override the move method
        public override void Move()
        {
            x += 6;
            y += 6;
        }
    }
    public class PlayerBase : Movable
    {
        // Player properties goes here
        protected int x, y;
        public  virtual void Move()
        {
            // same code as EnemyBase move method
            x += 2;
            y += 2;
        }
    }
    public class PlayerWithMoveType2 : PlayerBase
    {
        // override the move method
        public override void Move()
        {
            // same code as EnemyWithMoveType2 move method
            x += 4;
            y += 4;
        }
    }
    public class PlayerWithMoveType3 : PlayerBase
    {
        // override the move method
        public override void Move()
        {
            // same code as EnemyWithMoveType3 move method
            x += 6;
            y += 6;
        }
    }  

در ادامه نیز مدلسازی با ترکیب را میبینیم: 

     public interface IMovable
    {
        void Move(ref int x, ref int y);
    }
    public class EnemyBase
    {
        // Enemy properties goes here
        protected int x, y;
        IMovable moveBehavior;
        public void SetMoveBehavior(IMovable _moveBehavior) { moveBehavior = _moveBehavior; }
        public void Move()
        {
            moveBehavior.Move(ref x,ref y);
        }
    }
    public class PlayerBase
    {
        // Player properties goes here
        protected int x, y;
        IMovable moveBehavior;
        public void SetMoveBehavior(IMovable _moveBehavior) { moveBehavior = _moveBehavior; }
        public void Move()
        {
            moveBehavior.Move(ref x, ref y);
        }
    }
    public class MoveBehavior1
    {
        public void Move(ref int x, ref int y)
        {
            x += 2;
            y += 2;
        }
    }
    public class MoveBehavior2 : IMovable
    {
        public void Move(ref int x, ref int y)
        {
            x += 4;
            y += 4;
        }
    }
    public class MoveBehavior3 : IMovable
    {
        public void Move(ref int x, ref int y)
        { 
            x += 6;
            y += 6;
        }
    }  

همانطور که میبینید، با فراخوانی تابع SetMoveBehavior میتوان رفتار را در زمان اجرا تغییر داد.

در مقاله‌ی بعدی به ادامه‌ی اصول خواهم پرداخت. از شنیدن نظرات و سوالات شما خرسند خواهم شد.

مطالب
SharePoint Client object Model
دو روش اصلی برای دسترسی به داده‌ها از طریق برنامه نویسی در SharePoint وجود دارند. روش اول استفاده از SharePoint API روی سرور است. زمانیکه شما کدی را مستقیم روی سرور SharePoint  اجرا می‌کنید، SharePoint API کنترل کامل تمام جنبه‌های شیرپوینت و داده‌ها را در اختیار شما می‌گذارد. اگر برنامه شما روی سرور اجرا نمی‌شود و نیاز به دسترسی به داده‌های شیرپوینت دارد، لازم است از SharePoint web services استفاده کنید. web services امکاناتی مشابه SharePoint  API را در اختیار شما می‌گذارد؛ هرچند همه امکانات را پوشش نمی‌دهد.

در SharePoint 2010 گزینه دیگری در برنامه نویسی، برای دسترسی به داده‌های SharePoint تدارک دیده شده است: Client Object Model. این یک روش جدید، در برنامه نویسی شیرپوینت است. اگرچه استفاده از web services، پوشش وسیعی از امکانات شیرپوینت را به شما می‌دهد، اما برنامه نویسی به روش Client Object Model و API با استفاده از web services بسیار متفاوت است. استفاده از web services کار را برای شما سخت خواهد کرد و لازم است دو روش برنامه نویسی کاملا مختلف را بیاموزید. همچنین فراخوانی web services با JavaScript پیچیده است و نیازمند ساخت و دستکاری XML‌های فراوان است. Client Object Model تمام این مسائل را حل و برنامه نویسی سمت client را راحت کرده است.

در واقع Client Object Model سه Object Model جدا از هم است:
 نسخه: .NET CLR برای ساخت WinForms, Windows Presentation Foundation (WPF), console applications
 نسخه Silverlight : برای کا با هر دو حالت داخل in-browser و out-of-browser Silverlight applications
 نسخه JavaScript : کدهای Ajax و jQuery را قادر می‌سازد تا داده‌های شیرپوینت را فراخوانی کنند

یکی از سوالاتی که در مورد Client Object Model پیش می‌آید، این است که چه کارهایی را با آن می‌شود انجام داد؟ Client Object Model امکان دسترسی به بیشتر اشیاء رایج را مانند sites, webs, content types, lists, folders, navigations فراهم می‌کند. این اشیا با اسم‌های مشابه در Client Object Model وجود دارند که در جدول زیر مشخص شده‌اند.



 در زیر یک مثال ساده از استفاده‌های Client Object Model را توضیح خواهم داد که لیست‌های موجود در سایت را در خروجی نمایش می‌دهد.
1- در Visual Studio یک پروژه Console application ایجاد کنید.
2- بر روی References کلیک راست کرده Add Reference را انتخاب کنید. از مسیر زیر
 C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI
دو فایل زیر را اضافه کنید
 Microsoft.SharePoint.dll
Microsoft.SharePoint.Client.Runtime.dll

static void Main(string[] args)
        {
            var ctx = new ClientContext(@"http://localhost");
            var web = ctx.Web;
            var lists = web.Lists;
            ctx.Load(lists,
                l => l.Include
                    (list => list.Title).Where
                    (list => list.BaseType == BaseType.GenericList));
            ctx.ExecuteQuery();
            foreach (var list in lists)
                Console.WriteLine(list.Title);
            Console.ReadLine();
}
مطالب
آموزش TypeScript #1
با گسترش روز افزون زبان برنامه نویسی Javascript و استفاده هر چه بیشتر آن در تولید برنامه‌های تحت وب این زبان به یکی از قدرت‌های بزرگ در تولید برنامه‌های مبتنی بر وب تبدیل شده است. ترکیب این زبان با Css و Html5 تقریبا هر گونه نیاز برای تهیه و توسعه برنامه‌های وب را حل کرده است. جاوا اسکریپت در ابتدا برای اسکریپت نویسی سمت کلاینت برای صفحات وب ایجاد شد و برای سال‌ها به‌عنوان ابزاری برای مدیریت کردن رویدادهای صفحات وب محدود شده بود  و در نتیجه بسیاری از امکانات لازم برای برنامه‌نویسی برنامه‌های مقیاس بزرگ را به‌همراه نداشت. امروزه به قدری Javascript  توسعه داده شده است که حتی در تولید برنامه‌های Native مانند Windows Store  و برنامه‌های تحت Cloud نیز استفاده می‌شود. پیشرفت‌های صورت گرفته و اشاره شده در این حوزه موجب شد تا شاهد پیداش برنامه‌های مبتنی بر جاوا اسکریپت با سایزهای بی سابقه‌ای باشیم و این بیانگر این بود که تولید برنامه‌های مبتنی بر جاوا اسکریپت در مقیاس‌های بزرگ امر دشواری است و اینک TypeScript توسط غول نرم افزاری جهان پا به عرصه گذاشته که این فرآیند را آسان‌تر نماید. به کمک TypeScript می‌توان برنامه تحت JavaScript در مقیاس بزرگ تولید کرد به طوری با هر مرورگر و سیستم عاملی سازگار باشد. TypeScript از شی گرایی نیز پشتیبانی می‌کند و خروجی آن در نهایت به JavaScript کامپایل می‌شود. خیلی‌ها عقیده دارند که هدف اصلی مایکروسافت از تولید و توسعه این زبان رقابت با CoffeeScript است.  CoffeeScript یک زبان متن باز است که در سال 2009 توسط Jeremy Ashkenas ایجاد شده است و سورس آن در GitHub موجود می‌باشد. در آینده، بیشتر به مباحث مربوط به CoffeeScript و آموزش آن خواهم پرداخت.

در تصویر ذیل یک مقایسه کوتاه بین CoffeeScript و TypeScript را مشاهده می‌کنید.

با TypeScript چه چیزهایی به دست خواهیم آورد؟

یک نکته مهم این است که این زبان به خوبی در Visual Studio پشتیبانی می‌شود و قابلیت Intellisense نوشتن برنامه به این زبان را دلپذیرتر خواهد کرد و از طرفی دیگر به نظر من یکی از مهم‌ترین مزیت هایی که TypeScript در اختیار ما قرار می‌دهد این است که می‌توانیم به صورت Syntax آشنای شی گرایی کد نویسی کنیم و خیلی راحت‌تر کد‌های خود را سازمان دهی کرده و از نوشتن کد‌های تکراری اجتناب کنیم.

یکی دیگر از مزیت‌های مهم این زبان این است که این زبان از Static Typing به خوبی پشتیبانی می‌کند. این بدین معنی است که شما ابتدا باید متغیر‌ها را تعریف کرده و نوع آن‌ها را مشخص نمایید و هم چنین در هنگام پاس دادن مقادیر به پارامتر‌های توابع باید حتما به نوع داده ای آن‌ها دقت داشته باشید چون کامپایلر بین انواع داده ای در TypeScript تمایز قایل است و در صورت رعایت نکردن این مورد شما با خطا مواجه خواهید شد. این تمایز قایل شدن باعث می‌شود که برنامه هایی خواناتر داشته باشیم از طرفی باعث می‌شود که خطا یابی و نوشتن تست برای برنامه راحت‌تر و تمیزتر باشد. بر خلاف JavaScript، در TypeScript(به دلیل پشتیبانی از شی گرایی) می‌توانیم علاوه بر داشتن کلاس، اینترفیس نیز داشته باشیم و در حال حاضر مزایای استفاده از اینترفیس بر کسی پوشیده نیست.

به دلیل اینکه کد‌های TypeScript ابتدا کامپایل شده و بعد تبدیل به کد‌های JavaScript می‌شوند در نتیجه قبل از رسیدن به مرحله اجرای پروژه، ما از خطاهای موجود در کد خود مطلع خواهیم شد.

البته این نکته را نیز فراموش نخواهیم کرد که این زبان تازه متولد شده است(سال 2012 توسط Anders Hejlsberg) و همچنان در حال توسعه است و این در حال حاضر مهم‌ترین عیب این زبان می‌تواند باشد چون هنوز به پختگی سایر زبان‌های اسکریپتی در نیامده است.

در ذیل یک مثال کوچک به زبان TypeScript و JavaScript را برای مقایسه در خوانایی و راحتی کد نویسی قرار دادم:

TypeScript:

class Greeter {
    greeting: string;

    constructor (message: string) {
        this.greeting = message;
    }

    greet() {
        return "Hello, " + this.greeting;
    }
}
بعد از کامپایل کد بالا به کدی معادل زیر در JavaScript تبدیل خواهد شد:
var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();
توضیح چند واژه در TypeScript

Program : یک برنامه TypeScript مجموعه ای از یک یا چند Source File است. این Source File‌ها شامل کد‌های پیاده سازی برنامه هستند ولی در خیلی موارد برای خوانایی بیشتر برنامه می‌توان فقط تعاریف را در این فایل‌های سورس قرار داد.
Module: ماژول در TypeScript شبیه به مفاهیم فضای نام یا namespace در دات نت است و می‌تواند شامل چندین کلاس یا اینترفیس باشد.
Class : مشابه به مفاهیم کلاس در دات نت است و دقیقا همان مفهوم را دارد. یک کلاس می‌تواند شامل چندین تابع و متغیر با سطوح دسترسی متفاوت باشد. در TypeScript مجاز به استفاده از کلمات کلیدی public و private نیز می‌باشید. یک کلاس در Typescript می‌تواند یک کلاس دیگر را توسعه دهد(ارث بری در دات نت) و چندین اینترفیس را پیاده سازی نماید.
Interface: یک اینترفیس فقط شامل تعاریف است و پیاده سازی در آن انجام نخواهد گرفت. یک اینترفیس می‌تواند چندین اینترفیس دیگر را توسعه دهد.
Function: معادل متد در دات نت است. می‌تواند پارامتر ورودی داشته باشد و در صورت نیاز یک مقدار را برگشت دهد.
Scope: دقیقا تمام مفاهیم مربوط به محدوده فضای نام و کلاس و متد در دات نت در این جا نیز صادق است. 


آماده سازی Visual Studio برای شروع به کار
در ابتدا باید Template مربوطه به TypeScript را نصب کنید تا از طریف VS.Net بتوانیم به راحتی به این زبان کد نویسی کنیم. می‌توانید فایل نصب را از اینجا دانلود کنید. بعد از نصب از قسمت Template‌های موجود گزینه Html Application With TypeScript را انتخاب کنید

یا از قسمت Add  در پروژه‌های وب خود نظیر MVC گزینه TypeScript File را انتخاب نمایید.


در پست بعدی کد نویسی با این زبان را آغاز خواهیم کرد. 

مطالب
اندازه گیری کارآیی کدها توسط NBench
این روزها جهت اندازه‌گیری کارآیی قطعات کدهای دات نتی، استفاده از فریم ورک‌های مخصوصی که بسیاری از نکات ریز مرتبط با اینگونه اندازه‌گیری‌ها را مانند warmup یا گرم کردن JIT (جهت عدم اندازه گیری زمان کامپایل پویای کدها، بجای زمان واقعی اجرای آن‌ها)، اندازه‌گیری فشار بر روی Garbage collector و غیره را انجام می‌دهند، بجای استفاده‌ی از Stop Watch، متداول است. یکی از معروفترین‌های این گروه، که تقریبا حالت استانداردی را در جهت اندازه گیری کارآیی کدهای دات نتی پیدا کرده‌است، فریم ورک سورس باز NBench است.


شروع به کار با NBench

برای شروع به کار با NBench، ابتدا نیاز است دو بسته‌ی نیوگت ذیل را نصب کرد:
PM> Install-Package NBench
PM> Install-Package NBench.Runner
عملکرد این فریم ورک، شبیه به عملکرد فریم ورک‌های آزمون‌های واحد است. برای مثال فرض کنید که می‌خواهید فشار حافظه و فشار بر روی GC قطعه کدی را اندازه گیری کنید:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>();
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
 
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[GcMeasurement(GcMetric.TotalCollections, GcGeneration.AllGc)]
public void MeasureGarbageCollections()
{
    var dataCache = new List<int[]>();
    for (var i = 0; i < 500; i++)
    {
        for (var j = 0; j < 10000; j++)
        {
            var data = new int[100];
            dataCache.Add(data.ToArray());
        }
 
        dataCache.Clear();
    }
}
همانند نوشتن متدهای آزمون‌های واحد، ابتدا یک یا چند متد public void را در اینجا اضافه می‌کنیم.
سپس هر متد تست به ویژگی PerfBenchmark مزین می‌شود. در اینجا RunMode.Iterations به این معنا است که خودمان قصد داریم در طی یک حلقه، تعداد بار انجام را مشخص کنیم.
ویژگی MemoryMeasurement برای اندازه گیری حافظه‌ی مصرفی یک قطعه کد و GcMeasurement برای اندازه گیری فشار بر روی Garbage collector بکار می‌رود.


اجرای آزمون‌های NBench

پس از تهیه‌ی دو متد فوق، به پوشه‌ی packages\NBench.Runner.0.3.4\lib\net45 مراجعه کنید. یک فایل exe در آن موجود است که کار یافتن و اجرای آزمون‌های NBench را انجام می‌دهد. به عنوان پارامتر آن تنها کافی است مسیر اسمبلی برنامه (فایل exe و یا dll) را به آن ارسال کنیم:
 D:\Prog\NBenchSample\packages\NBench.Runner.0.3.4\lib\net45\NBench.Runner.exe "D:\Prog\NBenchSample\NBenchSample\bin\Release\NBenchSample.exe"
پس از آن، کار اجرای آزمون‌های NBench شروع شده و پس از مدتی ابتدا BEGIN WARMUP و END WARMUP‌ها را می‌توان مشاهده کرد و در آخر یک چنین خروجی ارائه می‌شود:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement ---------------
TotalBytesAllocated: Max: 47,842,944.00 bytes, Average: 42,002,757.60 bytes, Min: 41,353,848.00 bytes, StdDev: 2,052,032.33 bytes
TotalBytesAllocated: Max / s: 359,074,078.19 bytes, Average / s: 311,474,786.96 bytes, Min / s: 300,926,928.79 bytes, StdDev / s: 16,869,581.62 bytes

--------------- RESULTS: NBenchSample.Program+MeasureGarbageCollections ---------------
TotalCollections [Gen0]: Max: 708.00 collections, Average: 702.80 collections, Min: 697.00 collections, StdDev: 3.65 collections
TotalCollections [Gen0]: Max / s: 111.55 collections, Average / s: 109.87 collections, Min / s: 107.88 collections, StdDev / s: 1.28 collections

TotalCollections [Gen1]: Max: 338.00 collections, Average: 334.60 collections, Min: 330.00 collections, StdDev: 2.41 collections
TotalCollections [Gen1]: Max / s: 53.61 collections, Average / s: 52.31 collections, Min / s: 51.10 collections, StdDev / s: 0.70 collections

TotalCollections [Gen2]: Max: 32.00 collections, Average: 24.80 collections, Min: 18.00 collections, StdDev: 4.73 collections
TotalCollections [Gen2]: Max / s: 4.91 collections, Average / s: 3.87 collections, Min / s: 2.86 collections, StdDev / s: 0.72 collections


نکته‌ای در مورد اندازه گیری فشار حافظه

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

شبیه به همین استراتژی، در پیاده سازی Dictionary نیز بکارگرفته شده‌است:
[PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Measurement)]
[MemoryMeasurement(MemoryMetric.TotalBytesAllocated)]
public void AddMemoryMeasurement_With_initial_Size()
{
    const int numberOfAdds = 1000000;
    var dictionary = new Dictionary<int, int>(numberOfAdds);
    for (var i = 0; i < numberOfAdds; i++)
    {
        dictionary.Add(i, i);
    }
}
اگر اینبار این آزمون را انجام دهیم، به نتیجه‌ی ذیل خواهیم رسید:
 --------------- RESULTS: NBenchSample.Program+AddMemoryMeasurement_With_initial_Size ---------------
TotalBytesAllocated: Max: 23,245,912.00 bytes, Average: 23,245,912.00 bytes, Min: 23,245,912.00 bytes, StdDev: 0.00 bytes
TotalBytesAllocated: Max / s: 394,032,435.34 bytes, Average / s: 389,108,363.43 bytes, Min / s: 378,502,981.34 bytes, StdDev / s: 5,575,519.09 bytes
در اینجا زمانیکه شیء دیکشنری ایجاد شده‌است، اندازه‌ی اولیه‌ی آن نیز مشخص گردیده‌است. همین مساله سبب شده‌است تا مصرف حافظه‌ی آن از نزدیک به 41 مگابایت (متد AddMemoryMeasurement ابتدای بحث) به نزدیک 24 مگابایت (متد AddMemoryMeasurement_With_initial_Size فوق) کاهش یابد.
علت اینجا است که دیکشنری در پشت صحنه، از یک متد ReSize استفاده می‌کند که شبیه به سیستم عامل، بیشتر از مقدار مورد نیاز جهت ذخیره‌ی اشیاء، برای کاهش تعداد بار تخصیص‌های حافظه، حافظه به خود اختصاص می‌دهد. به همین جهت زمانیکه اندازه‌ی اولیه را مشخص کرد‌ه‌ایم، کار تخصیص حافظه‌ی بیش از اندازه‌ی این شیء، به شدت کاهش یافته‌است.


بررسی متد MeasureGarbageCollections

در متد MeasureGarbageCollections، مقدار زیادی شیء بر روی heap ایجاد شده و GC را وادار به عکس العمل شدید می‌کند.
حلقه‌ی داخلی ایجاد شده نیز تعداد زیادی شیء را در جهت پاکسازی GC تخصیص می‌دهد. این پاکسازی در مرحله‌ا‌ی به نام generation 0 صورت می‌گیرد.
اشیاء اضافه شده‌ی به لیست، طول عمر بیشتری دارند (تا پایان حلقه). بنابراین از garbage collection at generation 0 جان سالم به در خواهند برد و در garbage collection at generation 1  به عمر آن‌ها پایان داده خواهد شد. هرچند ممکن است تعدادی از آن‌ها پاکسازی نشده و تا پایان full garbage collection (generation 2) باقی بمانند.
در آزمایش انجام شده، با ذکر GcGeneration.AllGc، هر سه مورد Gen0 تا Gen2 اندازه گیری خواهند شد. عموما اندازه گیری Gen0 و Gen1 مهم نیستند و این‌ها خیلی زود به پایان خواهند رسید. اگر تعداد بار رخ‌دادن Gen2 زیاد بود (یا اصلا وجود داشت)، می‌تواند سبب بروز مشکلات کارآیی شدیدی گردد.
بنابراین می‌توان بجای تنظیم GcGeneration.AllGc، صرفا از GcGeneration.Gen2 استفاده کرد.


اندازه‌گیری Throughput یا تعداد بار اجرای یک متد در ثانیه

روش دیگر کار با فریم ورک NBench، ایجاد یک کلاس مخصوص و سپس افزودن متدهای Setup مزین به PerfSetup، متد Cleanup مزین به PerfCleanup و سپس تعدادی متد اندازه گیری کارآیی توسط ویژگی PerfBenchmark است. در اینجا برای اندازه‌گیری سرعت اجرای متدها، از ویژگی CounterThroughputAssertion استفاده خواهد شد که پارامتر اول آن نام یک شمارشگر است. این شمارشگر در متد Setup ایجاد می‌شود (با یک نام دلخواه).
public class DictionaryThroughputTests
{
    private readonly Dictionary<int, int> _dictionary = new Dictionary<int, int>();
 
    private const string AddCounterName = "AddCounter";
    private Counter _addCounter;
    private int _key;
 
    private const int AverageOperationsPerSecond = 20000000;
 
    [PerfSetup]
    public void Setup(BenchmarkContext context)
    {
        _addCounter = context.GetCounter(AddCounterName);
        _key = 0;
    }
 
    [PerfBenchmark(RunMode = RunMode.Throughput, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_ThroughputMode(BenchmarkContext context)
    {
        _dictionary.Add(_key++, _key);
        _addCounter.Increment();
    }
 
    [PerfBenchmark(RunMode = RunMode.Iterations, TestMode = TestMode.Test)]
    [CounterThroughputAssertion(AddCounterName, MustBe.GreaterThan, AverageOperationsPerSecond)]
    public void AddThroughput_IterationsMode(BenchmarkContext context)
    {
        for (var i = 0; i < AverageOperationsPerSecond; i++)
        {
            _dictionary.Add(i, i);
            _addCounter.Increment();
        }
    }
 
    [PerfCleanup]
    public void Cleanup(BenchmarkContext context)
    {
        _dictionary.Clear();
    }
}
در این آزمایش‌ها، RunMode.Throughput به معنای اجرای متد آزمایش به تعداد AverageOperationsPerSecond توسط فریم ورک NBench است. در حالت قید RunMode.Iterations، تعداد بار اجرا، توسط حلقه‌ای که ما مشخص کرده‌ایم، تعیین می‌گردد.
 --------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_ThroughputMode ---------------
[Counter] AddCounter: Max: 575,654.00 operations, Average: 575,654.00 operations, Min: 575,654.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,205,997.59 operations, Average / s: 7,163,894.30 operations, Min / s: 7,075,316.79 operations, StdDev / s: 42,518.20 operations

--------------- RESULTS: NBenchSample.DictionaryThroughputTests+AddThroughput_IterationsMode ---------------
[Counter] AddCounter: Max: 20,000,000.00 operations, Average: 20,000,000.00 operations, Min: 20,000,000.00 operations, StdDev: 0.00 operations
[Counter] AddCounter: Max / s: 7,409,380.61 operations, Average / s: 7,250,991.24 operations, Min / s: 6,880,938.73 operations, StdDev / s: 148,085.19 operations
اگر دقت کنید، کارآیی اندازه گیری شده‌ی در حالت RunMode.Iterations بیشتر است از حالت RunMode.Throughput. چون در حالت RunMode.Throughput، فریم ورک کار اجرای متد را از طریق Reflection انجام می‌دهد. بنابراین بهتر است از حالت RunMode.Iterations، جهت رسیدن به نتایج دقیق‌تری استفاده کرد.
در اینجا برای گزارش دادن، عددهای Average و  Average / s باید مورد استفاده قرار گیرند.