اشتراک‌ها
عرضه ASP.NET Identity 2.1.0-alpha1
خوشبختانه در این نگارش پیشنهاد متود الحاقی  ()<GetUserId<T  را داده بودم که اضافه شده است.
حالا با سفارشی سازی ASP.NET Identity، از این متود الحالی به راحتی می‌توانیم UserId را بدست بیاوریم
User.Identity.GetUserId<int>();
عرضه ASP.NET Identity 2.1.0-alpha1
مطالب
بررسی مفهوم Event bubbling در جی کوئری و تاثیر آن بر کارآیی کدهای نوشته شده
Event bubbling یا جوشیدن رویدادها به مفهوم انتقال رویدادهای رخ داده در یک المنت به سمت المنت یا المنت‌های والد می‌باشد. برای مثال با کلیک بر روی یک المنت در صفحه، رویداد کلیک هم در همان المنت اجرا خواهد شد و هم در المنت‌های والد.
ساختار سند زیر را در نظر بگیرید:
<div id="parent">
    <div id="child1">
        <div id="child2">
            <div id="child3"></div>
        </div>
    </div>
</div>
حال اگر برای هرکدام از divهای موجود در سند، یک هندلر برای مدیریت رویداد کلیک نوشته شود و کاربر بر روی child3 کلیک کند، به ترتیب ابتدا رویداد مربوط به المنت child3 سپس child2 سپس child1 و در نهایت parent اجرا خواهد شد. یعنی با کلیک بر روی child3، تمامی هندلرهای کلیک اجرا خواهند شد. دلیل اینکار همان مفهوم Event bubbling است.
Event bubbling فقط مختص صفحات وب نیست؛ بلکه در تمامی سیستم عامل‌ها یکی از مفاهیم مدیریت رخدادها(Events) است. حتی در برنامه‌های مبتنی بر ویندوز فرم هم شما با این مفهوم برخورد کرده‌اید.
در صفحات وب، در نهایت رویدادها به شیء Window منتقل می‌شوند و در یک وب فرم، به From اصلی برنامه.
حال با این مقدمه به سراغ بهینه سازی کدهای نوشته شده‌ی خود می‌رویم. اگر از کتابخانه‌ی جی‌کوئری استفاده کرده باشید، حتما از رویدادهای مختلف ماوس و صفحه کلید بهره برده‌اید. تصور برنامه‌ای که از رویدادها استفاده نکند و باید با کاربر در تعامل باشد، غیرممکن است؛ زیرا این رویدادها هستند که درخواست‌های کاربر را به برنامه منتقل می‌کنند.
به قطعه کد زیر توجه کنید:
$('#parent').on('click', function (event) {

});

$('#child1').on('click', function (event) {

});

$('#child2').on('click', function (event) {

});
ما یک هندلر برای مدیریت رویداد کلیک المنت parent نوشته‌ایم؛ یکی برای المنت child1 و یکی دیگر برای child2. با استفاده از مفهوم جوشیدن رخدادها می‌توانیم هر سه هندلر را حذف و به یک هندلر تبدیل کنیم!
$(document).on('click', '#parent, #child1, #child2, #child3', function (event) {

});
شاید بپرسید مزیت اینکار چیست؟ نکته‌ی کلیدی در همینجاست. میزان حافظه‌ی مصرفی مدیریت یک رخداد، به مراتب کمتر از چندین رخداد است.
در واقع شما فقط یک هندلر را ثبت و تمامی کارهای لازم را به آن می‌سپارید. شدیدا توصیه می‌شود که در نوشتن کدهای خود از ایجاد هندلر بر روی هر عنصر خودداری کنید.
برای مثال اگر شما در صفحه‌ی مدیریت پست‌ها قرار دارید و برای ویرایش هر پست دکمه‌ای را تعیین کرده باشید به جای نوشتن کدی مانند زیر:
$('.post .edit').on('click', function (event) {

});
از نسخه‌ی بهینه شده‌ی آن استفاده کنید:
$(document).on('click', '.post .edit', function (event) {

});
تصور کنید شما در همین صفحه 50 پست را به کاربر نشان داده باشید و اگر از کد بالا استفاده کنید، به ازای هر 50 دکمه‌ی ویرایش، یک هندلر برای رویداد کلیک خواهید داشت. حالا اگر از کد پایین استفاده کنید، تنها یک هندلر برای 50 رویداد خواهید داشت.
همان صفحه‌ی مدیریت پست را در نظر بگیرید. 50 پست داریم. هر کدام یک دکمه‌ی ویرایش، حذف، امتیازات، کامنتها و کلی ابزار دیگر که همه با رویداد کلیک فعال می‌شوند. چیزی حدود به 300 رویداد را باید ثبت کنید!
این واقعا یک تراژدی بزرگ در مصرف حافظه محسوب می‌شود. پس بهینه‌تر است تا با نوشتن یک رویداد کلیک روی کل شیء سند، از ایجاد هندلرهای اضافی خودداری کنید.

در اینجا دو نکته قابل ذکر است:
1- چگونه از Event bubbling جلوگیری کنیم؟
برخی از اوقات لازم است تا در لایه‌های تو در تو، به ازای هر لایه، کد خاصی اجرا شود. یعنی با کلیک بر روی child3 نمی‌خواهیم رویداد مربوط به parent یا حتی child2 اجرا شوند. در این حالت باید از event.stopPropagation در بدنه‌ی هندلر استفاده کنیم.

2- چگونه می‌توان تشخیص داد که بر روی کدام لایه یا المنت کلیک شده است؟
شما با استفاده از event.event.target، به شیء هدف دسترسی خواهید داشت. برای مثال اگر قصد داشته باشیم که قسمتی از کدهای ما فقط بر روی یک المنت خاص اجرا شوند، می‌توانیم به شکل زیر آنها را تفکیک کنیم:
        var elemnt = $(event.target);
        if (elemnt.attr('id') === 'parent') {
            alert('this is parnet');
        }
        else if (elemnt.attr('id') === 'child2') {
            alert('this is child2');
        }
البته نوشتن شرط برای همه‌ی المنت‌ها در یک هندلر هم باعث طولانی شدن کدها و هم تولید کد اضافه خواهد شد. خوشبختانه جی کوئری، مدیریت و ثبت رخدادها را هوشمندانه انجام می‌دهد. به جای نوشتن شرط، به راحتی کدهای مربوط به هر المنت را در یک رجیستر کننده‌ی جدا بنویسید و در نهایت جی کوئری آن‌ها را برای شما به یک هندلر منتقل خواهد کرد:
$(document).ready(function () {

    $(document).on('click','#parent', function (event) {

    });

    $(document).on('click','#child1', function (event) {

    });

    $(document).on('click','#child2', function (event) {
        event.st
    });

});

یکی دیگر از مهمترین مزایای کدنویسی به شکل فوق اینست که حتی رویدادهای مربوط به اشیایی که به صورت پویا به سند اضافه می‌شوند، اجرا خواهند شد.
در صفحه‌ی اصلی همین سایت بر روی دکمه‌ی بارگزاری بیشتر کلیک کنید. پس از اضافه شدن پست‌ها سعی کنید به یک پست امتیاز دهید. اتفاقی نخواهد افتاد. زیرا برای عناصری که بصورت پویا به صفحه اضافه شده‌اند رویدادی ثبت نشده است، که اگر از کدهای فوق استفاده شود با کمترین هزینه به هدف دلخواه خود خواهیم رسید.
پس همیشه رویدادها را تا حد امکان بر روی عنصر ریشه تعریف کنید.
دیدن لینک زیر برای اجرای یک تست و درک بهتر مطلب خالی از لطف نخواهد بود:
http://jsperf.com/jquery-body-delegate-vs-document-delegate
نظرات مطالب
پیاده سازی Option یا Maybe در #C
منظور من رو متوجه نشدید. بذارید با کد توضیح بدم:
میخواهیم طبق هدف مقاله، این تکه کد را اصلاح کنیم.
public ActionResult Details(int id)
        {
            var user=_userService.GetById(3); // این متد ممکن است مقداری برگرداند و یا مقدار نال برگرداند
            if( user == null)
                 return HttpNotFound();
            return View(user);
        }
راهکار ارائه شده:
public ActionResult Details(int id)
        {
            var user = _userService
                            .GetById(3)
                            .DefaultIfEmpty(new User())
                            .Single();
            return View(user);
        }
پر واضح است خروجی این دو متد با هم یکسان نیستند.
راه حل ارائه شده کامل نیست و با تغییر صورت مساله، به جواب دیگری میرسد.
باید به کدی مثل این برسیم:
public ActionResult Details(int id)
{
    return 
        Search<ActionResult>(id)
        .OnExistValue(View("Details"))
        .OnNotExistValue(new HttpNotFoundResult())
        .ToValue();
}
برای نیل به این هدف:
public class Maybe<T, TResult> : IEnumerable<T>
    {
        private readonly T[] _data;
        private readonly TResult _result;

        private Maybe(T[] data)
        {
            _data = data;
        }

        private Maybe(TResult result)
        {
            _result = result;
        }

        public TResult ToValue() => _result;
        public Maybe<T, TResult> OnExistValue(TResult result) => _data.Any() ? new Maybe<T, TResult>(result) : this;

        public Maybe<T, TResult> OnNotExistValue(TResult result) => _result == null ? new Maybe<T, TResult>(result) : this; 

        public static Maybe<T, TResult> Create(T element) => new Maybe<T, TResult>(new[] {element});

    public static Maybe<T, TResult> CreateEmpty() => new Maybe<T, TResult>(new T[0]);

    public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>) _data).GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
}
و متد جستجو نیز ایچنین تغییر خواهد کرد:
public Maybe<User, TResult> Search<TResult>(int id)
{
    var lst = new User[] {};

    var r = lst.Where(x => x.Id == id).ToList();
    return r.Any() ? Maybe<User, TResult>.Create(r[0]) : Maybe<User, TResult>.CreateEmpty();
}
یکی از راه حل‌ها میتواند این کدها باشند.
اشتراک‌ها
دوره آموزشی چهار ساعته Blazor Hybrid

Learn Blazor Hybrid - Full Course for Beginners | Build cross-platform apps in C#

Let's start our journey together to build beautiful native cross-platform apps for iOS, Android, macOS, and Windows with Blazor Hybrid, .NET MAUI, C#, and Visual Studio! In this full workshop, I will walk you through everything you need to know about .NET MAUI and building your very first app. You will learn the basics including how to build user interfaces with Razor, how to show data from the internet, how to navigate between pages and combine .NET MAUI pages with Razor pages, access platform features like geolocation, and theme your app for light theme and dark theme. This course has everything you need to learn the basics and set you up for success when building apps with Blazor Hybrid!

Chapters:

00:00:00 - Intro to the Blazor Hybrid Workshop

00:04:28 - What is Blazor Hybrid & How to Install

00:06:51 - First Blazor Hybrid App & Architecture

00:21:40 - Get Code to Build Your First Blazor Hybrid App

00:26:38 - Blazor Hybrid Project Walkthrough

00:39:22 - Start to Build First Blazor Hybrid App

01:03:10 - Event Handling, Data Binding and Parameters (Slides)

01:09:00 - Add Monkey Data & Fluent UI Blazor Components

01:32:08 - Navigation, NavigationManager, .NET MAUI Pages (Slides)

01:39:19 - Navigation with NavigationManager

01:52:39 - Navigation with NavLinks

01:57:21 - Add .NET MAUI Pages & Components

02:21:11 - Access Platform Functionality (Slides)

02:27:57 - Check Network Connectivity

02:38:04 - Get User Location with Geolocation

02:49:09 - Integration with Other Apps

02:57:42 - App Theming, Light Theme, Dark Theme (Slides)

03:05:36 - JavaScript Interoperability with IJSRuntime

03:20:48 - Theming FluentUI Blazor Components

03:26:05 - Style Status Bar with .NET MAUI Community Toolkit

03:39:00 - .NET MAUI Light & Dark Theme with AppThemeBinding

03:42:58 - Sharing State & Creating Reusable Components (Slides)

03:47:27 - Implement Shared State Blazor Hybrid & .NET MAUI

04:02:47 - Create Reusable Razor Components

04:08:31 - CONGRATULATIONS!

دوره آموزشی چهار ساعته Blazor Hybrid
نظرات مطالب
مقایسه بین حلقه های تکرار (Lambda ForEach و for و foreach)
متد ForEach در کلاس List از حلقه for معمولی استفاده میکنه و نه foreach:
public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  for (int index = 0; index < this._size; ++index)
    action(this._items[index]);
}
متد Array.ForEach هم از روشی مشابه استفاده کرده:
public static void ForEach<T>(T[] array, Action<T> action)
{
  if (array == null)
    throw new ArgumentNullException("array");
  if (action == null)
    throw new ArgumentNullException("action");
  for (int index = 0; index < array.Length; ++index)
    action(array[index]);
}
foreach به دلیل استفاده از اشیای درون IEnumerable و درنتیجه اجرای دستورات بیشتر در هر حلقه کندتر عمل میکند.
اما! اگر هدف تنها بررسی سرعت اجرای حلقه‌های اشاره شده باشه متدهای بالا نتیجه درستی نشان نخواهد داد، چون عملیات انجام شده در حلقه‌های نشان داده شده با هم دقیقا یکسان نیست. بهتره که یه عملیات ثابت و مستقل از متغیرهای درگیر استفاده بشه تا نتایج دقیقتری بدست بیاد. مثلا یه چیزی مثل اکشن زیر:
() => { int a = 1; }
بهتره تو این تستها مشخصات دقیق سخت افزاری هم ارائه بشه تا مقایسه‌ها بهتر انجام بگیره.
با این شرح با روشی که در مطلب Microbenchmark آورده شده آزمایشات رو دوباره انجام دادم و برای تعداد تکرار 100 میلیون اختلاف تمام حلقه‌ها در حد چند میلی ثانیه بود که کاملا قابل صرفنظره!
نتایج برای حالات مختلف موجود تفاوتهای زیادی داشت اما درنسخه ریلیز نهایتا نتایج کلی این بود که حلقه for معمولی از همه سریعتر، سپس Array.ForEach و بعد متد ForEach در کلاس List و درنهایت ازهمه کندتر حلقه foreach بود.
من آزمایشات روی یک سیستم با پردازنده 4 هسته ای با کلاک 3.4 گیگاهرتز (AMD Phenom II 965) با ویندوز 7 و 32 بیتی با رم 4 گیگ (3.25 گیگ قایل استفاده)انجام دادم. متاسفانه تعداد تکرار بیشتر خطای OutOfMemory میداد.
نکته: اجرای تستهای این چنینی برای آزمایش کارایی و سرعت به شدت تحت تاثیر عوامل جانبی هستند. مثل میزان منابع در دسترس سخت افزاری، نوع سیستم عامل، برنامه‌ها و سرویس‌های در حال اجرا، و مهمتر از همه نوع نسخه بیلد شده از برنامه تستر (دیباگ یا ریلیز) و محل اجرای تست (منظور اجرا در محیط دیباگ ویژوال استودیو یا اجرای مستقل برنامه) و ... . (همونطور که آقای نصیری هم مطلبی مرتبط رو به اشتراک گذاشتند ^)
مطالب
چگونگی استفاده از افزونه Isotope در AngularJS

حتما تا به حال در وب سایت‌های زیادی قسمت هایی را دیده اید که چیدمان عناصر آن به شکل زیر است:

این گونه چیدمان را حتما در منوی Start ویندوز 8 بار‌ها دیده‌اید! عناصر تشکیل دهنده‌ی این شکل از چیدمان، می‌توانند یک سری عکس باشند که تشکیل یک گالری عکس را داده‌اند و یا یک سری div که محتوای پست‌های یک وبلاگ را در خود جای داده‌اند. چیزی که این شکل از چیدمان عناصر را نسبت به چیدمان‌های معمول متمایز می‌کند این است که طول و عرض هر یک از این عناصر با یکدیگر متفاوت است و هدف از این گونه چیدمان آن است که این عناصر در فضایی که به آن‌ها اختصاص داده شده است، به صورت بهینه قرار گیرند تا کمترین فضا هدر رود.

برای اعمال این شکل از چیدمان در دنیای وب افزونه‌های زیادی بر فراز کتاب خانه‌ی jQuery تدارک دیده شده است که از جمله مطرح‌ترین آن‌ها می‌توان به افزونه های Isotope ، Masonry و Gridster  اشاره کرد.

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

در این مقاله قصد من این است که نشان دهم چگونه از افزونه‌ی Isotope در AngularJS استفاده کنیم؛ چگونه چیدمان آن را راست به چپ کنیم و چگونه آن را با محیط‌های واکنش گرا (Responsive) سازگار کنیم.

فرض کنید در یک وب سایت قصد داریم اطلاعات یک سری مطلب خبری را از سرور، به فرمت JSON دریافت کرده و نمایش دهیم. در AngularJS شیوه‌ی کار بدین صورت است که اطلاعاتی که به فرمت JSON هستند را با استفاده از directive ایی به نام ng-repeat پیمایش کرده و آن‌ها را نمایش دهیم.  حال اگر بخواهیم چیدمان مطالب را با استفاده از Isotope تغییر دهیم، می‌بینیم که هیچ چیزی نمایش داده نمی‌شود. دلیل آن بر می‌گردد به مراحل کامپایل کردن AngularJS و نامشخص بودن زمان اعمال چیدمان Isotope به عناصر است.

در AngularJS هنگامیکه با دستکاری DOM سر و کار پیدا می‌کنیم، معمولا باید به سراغ Directive‌ها رفت و یک Directive سفارشی برای کار با Isotope تعریف کرد تا با مکانیزم‌های Angular سازگار باشد. خوشبختانه Directive Isotope برای Angular موجود می‌باشد. نکته‌ی مهم این است که این Directive برای نگارش 1 افزونه‌ی Isotope نوشته شده است. البته با نگارش 2 هم کار می‌کند که من برای انجام کار خود نسخه‌ی 1 را ترجیح دادم استفاده کنم.

نکته‌ی بعدی که باید رعایت شود این است که چیدمان عناصر باید از راست به چپ شوند. خوشبختانه این کار در نسخه‌ی 1 Isotope با تغییر کوچکی در سورس Isotope و تغییر یک تابع انجام میشود. گویا نسخه‌ی دوم امکان پیش فرضی را برای این کار دارد، اما نتوانستم آن را به خوبی پیاده سازی کنم و به همین دلیل ترجیح دادم از همان نسخه‌ی اول استفاده کنم.

برای اینکه در هنگام جابه جا شدن عناصر، انیمیشن‌ها نیز از راست به چپ انجام شوند، باید css‌های زیر را نیز اعمال نمود:

.isotope .isotope-item {
  -webkit-transition-property: right, top, -webkit-transform, opacity;
     -moz-transition-property: right, top, -moz-transform, opacity;
      -ms-transition-property: right, top, -ms-transform, opacity;
       -o-transition-property: right, top, -o-transform, opacity;
          transition-property: right, top, transform, opacity;
}

Responsive بودن این عناصر مسئله‌ی دیگری است که باید حل گردد. امروزه اکثر فریم ورک‌های مطرح css، واکنشگرا نیز هستند و برای پشتیبانی از سایز‌های متفاوت صفحه نمایش، تدابیری در نظر گرفته‌اند. اساس کار واکنش گرا بودن این فریم ورک‌ها در تعیین ابعاد عناصر، بیان ابعاد به صورت درصدی است. مثلا فلان عرض div برابر 50% باشد بدین معناست که همیشه عرض این div نصف عرض عنصر والد آن باشد.

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

  برای حل این مشکل می‌توان از امکانات CSS به مانند دستورات زیر استفاده کرد: 
@media (min-width: 768px) and (max-width: 980px) {
    .card {
        width: 320px;
    }
}

@media (min-width: 980px) and (max-width: 1200px) {
    .card {
        width: 260px;
    }
}

@media (min-width: 1200px) {
    .card {
        width: 340px;
    }
}
بدین صورت می‌توان در ابعاد مختلف نمایشگر تعیین کرد که عرض عناصر ما چقدر باشد.
اکنون یک گالری عکس را در نظر بگیرید که در زیر هر عکس توضیحی نیز نوشته شده است و ساختار HTML آن به این صورت است که داخل هر div عکسی نیز موجود است. اگر به شیوه‌ی ذکر شده عمل کنید با یک اشکال مواجه می‌شوید و عناصر روی هم قرار گرفته و اصطلاحا overlapping اتفاق می‌افتد. دلیل این امر این است که لود شدن عکس‌ها عملی زمان گیر است و Isotope قبل از این که عکس لود شود، سایز آن عنصر را محاسبه کرده که در حقیقت این سایز بدون احتساب سایز عکس است و ابعاد واقعی عنصر ما نیست؛ در نتیجه وقتی عکس لود می‌شود آن div فضای بیشتری احتیاج دارد و به همین دلیل به زیر div‌های دیگر می‌رود.
برای حل این مشکل باید به این صورت عمل کرد که وقتی عکس‌ها کامل لود شدند، Isotope وارد عمل شده و سایز عناصر را به دست آورده و آن‌ها را بچیند. برای این کار معمولا از افزونه‌ی  imagesLoaded استفاده می‌کنند که با کمک این افزونه می‌توان مشخص کرد که وقتی تمام عکس‌های موجود در فلان div کامل لود شدند، Isotope وارد عمل شده و عناصر را چیدمان کند.
البته بدون استفاده از افزونه‌ی imagesLoaded و به کمک امکانات AngularJS و تعریف یک Directive سفارشی می‌توان زمان لود شدن عکس‌ها را کنترل کرد.
app.directive('imageOnload', function () {
            return {
                restrict: 'A',
                link: function (scope, element, attrs) {
                    element.bind('load', function () {
                        scope.$emit('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                    });
                }
            };
        });
کار این directive این است که به ازای بارگذاری هر عکس، متد reLayout را از Isotope، فراخوانی می‌کند. از این جهت فراخوانی reLayout به ازای لود شدن هر عکس بهتر است که لود شدن تمامی عکس‌ها ممکن است مدت زمان زیادی طول بکشد و کاربر برای مدتی با یک ساختار بهم ریخته مواجه شود.
    
اگر در نمونه کدی که قرار داده‌ام، به انتهای کدهای کنترلر ListController دقت کنید، برای رویداد resize شی window، تابعی تعریف شده است تا به هنگام تغییر سایز صفحه فراخوانی شود. در این رویداد هر بار که سایز پنجره تغییر کرد، پس از یک ثانیه تابع reLayout  افزونه‌ی Isotope را فراخوانی می‌کنیم تا مجددا المنت‌های صفحه چیده شوند. البته ضرورتی وجود نداشته ولی در بعضی مواقع عناصر خوب چیده نمی‌شدند که با فراخوانی reLayout از چیدمان صحیح عناصر مطابق با سایز جدید صفحه اطمینان حاصل پیدا می‌کنیم. دلیل یک ثانیه تاخیر این است که اگر به ساز و کار تعاریف متد‌ها در directive Isotope دقت کنید، از سرویس timeout$  به وفور استفاده شده است. ظاهرا اگر برای فراخوانی reLayout زودتر عمل کنیم با فراخوانی هایی این متد در ساختار خودش تداخل پیدا می‌کند.
$(window).resize(function () {
                $timeout(function myfunction() {
                    $scope.$broadcast('iso-method', { name: 'reLayout', params: null }); // call reLayout isotope methode prevent overlaaping the items
                },1000);
                
            });
   
در نهایت تمامی نکات گفته شده را به صورت یک نمونه کد آماده کردم:
   

مطالب
ASP.NET MVC #20

تهیه گزارشات تحت وب به کمک WebGrid

WebGrid از ASP.NET MVC 3.0 به صورت توکار به شکل یک Html Helper در دسترس می‌باشد و هدف از آن ساده‌تر سازی تهیه گزارشات تحت وب است. البته این گرید، تنها گرید مهیای مخصوص ASP.NET MVC نیست و پروژه MVC Contrib یا شرکت Telerik نیز نمونه‌های دیگری را ارائه داده‌اند؛ اما از این جهت که این Html Helper، بدون نیاز به کتابخانه‌های جانبی در دسترس است، بررسی آن ضروری می‌باشد.


صورت مساله

لیستی از کارمندان به همراه حقوق ماهیانه آن‌ها در دست است. اکنون نیاز به گزارشی تحت وب، با مشخصات زیر می‌باشد:
1- گزارش باید دارای صفحه بندی بوده و هر صفحه تنها 10 ردیف را نمایش دهد.
2- سطرها باید یک در میان دارای رنگی متفاوت باشند.
3- ستون حقوق کارمندان در پایین هر صفحه، باید دارای جمع باشد.
4- بتوان با کلیک بر روی عنوان هر ستون، اطلاعات را بر اساس ستون انتخابی، مرتب ساخت.
5- لینک‌های حذف یا ویرایش یک ردیف نیز در این گزارش مهیا باشد.
6- لیست تهیه شده، دارای ستونی به نام «ردیف» نیست. این ستون را نیز به صورت خودکار اضافه کنید.
7- لیست نهایی اطلاعات، دارای ستونی به نام مالیات نیست. فقط حقوق کارمندان ذکر شده است. ستون محاسبه شده مالیات نیز باید به صورت خودکار در این گزارش نمایش داده شود. این ستون نیز باید دارای جمع پایین هر صفحه باشد.
8- تمام اعداد این گزارش در حین نمایش باید دارای جدا کننده سه رقمی باشند.
9- تاریخ‌های موجود در لیست، میلادی هستند. نیاز است این تاریخ‌ها در حین نمایش شمسی شوند.
10- انتهای هر صفحه گزارش باید بتوان برچسب «صفحه y/n» را مشاهده کرد. n در اینجا منظور تعداد کل صفحات است و y شماره صفحه جاری می‌باشد.
11- انتهای هر صفحه گزارش باید بتوان برچسب «رکوردهای y تا x از n» را مشاهده کرد. n در اینجا منظور تعداد کل رکوردها است.
12- نام کوچک هر کارمند، ضخیم نمایش داده شود.
13- به ازای هر شماره کارمندی، یک تصویر در پوشه images سایت وجود دارد. برای مثال images/id.jpg. ستونی برای نمایش تصویر متناظر با هر کارمند نیز باید اضافه شود.
14- به ازای هر کارمند، تعدادی پروژه هم وجود دارد. پروژه‌های متناظر را توسط یک گرید تو در تو نمایش دهید.


راه حل به کمک استفاده از WebGrid

ابتدا یک پروژه خالی ASP.NET MVC را آغاز کنید. سپس مدل‌های زیر را به آن اضافه نمائید (یک کارمند که می‌تواند تعداد پروژه منتسب داشته باشد):

using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public class Employee
{
public int Id { set; get; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime AddDate { get; set; }
public double Salary { get; set; }
public IList<Project> Projects { get; set; }
}
}

namespace MvcApplication17.Models
{
public class Project
{
public int Id { set; get; }
public string Name { set; get; }
}
}

سپس منبع داده نمونه زیر را به پروژه اضافه کنید. به عمد از ORM‌ خاصی استفاده نشده تا بتوانید پروژه جاری را به سادگی در یک پروژه آزمایشی جدید،‌ تکرار کنید.
using System;
using System.Collections.Generic;

namespace MvcApplication17.Models
{
public static class EmployeeDataSource
{
public static IList<Employee> CreateEmployees()
{
var list = new List<Employee>();
var rnd = new Random();
for (int i = 1; i <= 1000; i++)
{
list.Add(new Employee
{
Id = i + 1000,
FirstName = "fName " + i,
LastName = "lName " + i,
AddDate = DateTime.Now.AddYears(-rnd.Next(1, 10)),
Salary = rnd.Next(400, 3000),
Projects = CreateRandomProjects()
});
}
return list;
}

private static IList<Project> CreateRandomProjects()
{
var list = new List<Project>();
var rnd = new Random();
for (int i = 0; i < rnd.Next(1, 7); i++)
{
list.Add(new Project
{
Id = i,
Name = "Project " + i
});
}
return list;
}
}
}


در ادامه یک کنترلر جدید را با محتوای زیر اضافه نمائید:
using System.Web.Mvc;
using MvcApplication17.Models;

namespace MvcApplication17.Controllers
{
public class HomeController : Controller
{
[HttpPost]
public ActionResult Delete(int? id)
{
return RedirectToAction("Index");
}

[HttpGet]
public ActionResult Edit(int? id)
{
return View();
}

[HttpGet]
public ActionResult Index(string sort, string sortdir, int? page = 1)
{
var list = EmployeeDataSource.CreateEmployees();
return View(list);
}
}
}

علت تعریف متد index با پارامترهای sort و غیره به URLهای خودکاری از نوع زیر بر می‌گردد:

http://localhost:3034/?sort=LastName&sortdir=ASC&page=3

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

سپس یک View خالی را نیز برای متد Index ایجاد کنید. تا اینجا تنظیمات اولیه پروژه انجام شد.
کدهای کامل View را در ادامه ملاحظه می‌کنید:

@using System.Globalization
@model IList<MvcApplication17.Models.Employee>

@{
ViewBag.Title = "Index";
}

@helper WebGridPageFirstItem(WebGrid grid)
{
@(((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1));
}

@helper WebGridPageLastItem(WebGrid grid)
{
if (grid.TotalRowCount < (grid.PageIndex + 1 * grid.RowsPerPage))
{
@grid.TotalRowCount;
}
else
{
@((grid.PageIndex + 1) * grid.RowsPerPage);
}
}

<h2>Employees List</h2>

@{
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

<div id="container">
@grid.GetHtml(
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },
mode: WebGridPagerModes.All,
columns: grid.Columns(
grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),
grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),
grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),
grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");

}),
grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),
grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),
grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))
)
)

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount,
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

@*
@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}
*@
</div>

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}


توضیحات ریز جزئیات View فوق


تعریف ابتدایی شیء WebGrid و مقدار دهی آن
در ابتدا نیاز است یک وهله از شیء WebGrid را ایجاد کنیم. در اینجا می‌توان تنظیم کرد که آیا نیاز است اطلاعات نمایش داده شده دارای صفحه بندی (canPage) خودکار باشند؟ منبع داده (source) کدام است. در صورت فعال سازی صفحه بندی خودکار، چه تعداد ردیف (rowsPerPage) در هر صفحه نمایش داده شود. آیا نیاز است بتوان با کلیک بر روی سر ستون‌ها، اطلاعات را بر اساس فیلد متناظر با آن مرتب (canSort) ساخت؟ همچنین در صورت نیاز به مرتب سازی، اولین باری که گرید نمایش داده می‌شود، بر اساس چه فیلدی (defaultSort) باید مرتب شده نمایش داده شود:

@{ 
var grid = new WebGrid(
source: Model,
canPage: true,
rowsPerPage: 10,
canSort: true,
defaultSort: "FirstName"
);
var salaryPageSum = 0;
var taxPageSum = 0;
var rowIndex = ((grid.PageIndex + 1) * grid.RowsPerPage) - (grid.RowsPerPage - 1);
}

در اینجا همچنین سه متغیر کمکی هم تعریف شده که از این‌ها برای تهیه جمع ستون‌های حقوق و مالیات و همچنین نمایش شماره ردیف جاری استفاده می‌شود. فرمول نحوه محاسبه اولین ردیف هر صفحه را هم ملاحظه می‌کنید. شماره ردیف‌های بعدی، rowIndex++ خواهند بود.


تعریف رنگ و لعاب گرید نمایش داده شده
در ادامه به کمک متد grid.GetHtml، رشته‌ای معادل اطلاعات HTML صفحه جاری، بازگشت داده می‌شود. در اینجا می‌توان یک سری خواص تکمیلی را تنظیم نمود. برای مثال:
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style",
htmlAttributes: new { id = "MyGrid" },

هر کدام از این رشته‌ها در حین رندر نهایی گرید،‌ تبدیل به یک class خواهند شد. برای نمونه:

<div id="container">
<table class="webgrid" id="MyGrid">
<thead>
<tr class="webgrid-header">

به این ترتیب با اندکی ویرایش css سایت، می‌توان انواع و اقسام رنگ‌ها را به سطرها و ستون‌های گرید نهایی اعمال کرد. برای مثال اطلاعات زیر را به فایل css سایت اضافه نمائید:

/* Styles for WebGrid
-----------------------------------------------------------*/
.webgrid
{
width: 100%;
margin: 0px;
padding: 0px;
border: 0px;
border-collapse: collapse;
font-family: Tahoma;
font-size: 9pt;
}

.webgrid a
{
color: #000;
}

.webgrid-header
{
padding: 0px 5px;
text-align: center;
border-bottom: 2px solid #739ace;
height: 20px;
border-top: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-header th
{
background-color: #eaf0ff;
border-right: 1px solid #ddd;
}

.webgrid-footer
{
padding: 6px 5px;
text-align: center;
background-color: #e8eef4;
border-top: 2px solid #3966A2;
height: 25px;
border-bottom: 2px solid #D6E8FF;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-alternating-row
{
height: 22px;
background-color: #f2f2f2;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-row-style
{
height: 22px;
border-bottom: 1px solid #d2d2d2;
border-left: 2px solid #D6E8FF;
border-right: 2px solid #D6E8FF;
}

.webgrid-selected-row
{
font-weight: bold;
}

.text-align-center-col
{
text-align: center;
}

.total-row
{
background-color:#f9eef4;
}

همانطور که ملاحظه می‌کنید، رنگ‌های ردیف‌ها، هدر و فوتر گرید و غیره در اینجا تنظیم می‌شوند.
به علاوه اگر دقت کرده باشید در تعاریف گرید، htmlAttributes هم مقدار دهی شده است. در اینجا به کمک یک anonymously typed object، مقدار id گرید مشخص شده است. از این id در حین کار با jQuery‌ استفاده خواهیم کرد.


تعیین نوع Pager
پارامتر دیگری که در متد grid.GetHtml تنظیم شده است، mode: WebGridPagerModes.All می‌باشد. WebGridPagerModes یک enum با محتوای زیر است و توسط آن می‌توان نوع Pager گرید را تعیین کرد:

[Flags]
public enum WebGridPagerModes
{
Numeric = 1,
//
NextPrevious = 2,
//
FirstLast = 4,
//
All = 7,
}

نحوه تعریف ستون‌های گرید
اکنون به مهم‌ترین قسمت تهیه گزارش رسیده‌ایم. در اینجا با مقدار دهی پارامتر columns، نحوه نمایش اطلاعات ستون‌های مختلف مشخص می‌گردد. مقداری که باید در اینجا تنظیم شود، آرایه‌ای از نوع WebGridColumn می‌باشد و مرسوم است به کمک متد کمکی grid.Columns،‌ اینکار را انجام داد.
متد کمکی grid.Column، یک وهله از شیء WebGridColumn را بر می‌گرداند و از آن برای تعریف هر ستون استفاده خواهیم کرد. توسط پارامتر columnName آن،‌ نام فیلدی که باید اطلاعات ستون جاری از آن اخذ شود مشخص می‌شود. به کمک پارامتر header،‌ عبارت سرستون متناظر تنظیم می‌گردد. پارامتر format، مهم‌ترین و توانمندترین پارامتر متد grid.Column است:

grid.Column(columnName: "FirstName", header: "First Name",
format: @<span style='font-weight: bold'>@item.FirstName</span>,
style: "text-align-center-col"),
grid.Column(columnName: "LastName", header: "Last Name"),

پارامتر format، به نحو زیر تعریف شده است:

Func<dynamic, object> format

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

format: @<span style='font-weight: bold'>@item.FirstName</span>
or
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
}

مستقیما از توانمندی‌های Razor استفاده کنید. مثلا یک تگ کامل را بدون نیاز به محصور سازی آن بین "" شروع کنید. سپس @item به وهله‌ای از رکورد در دسترس اشاره می‌کند که در اینجا وهله‌ای از شیء کارمند است.
و یا همانند روشی که برای محاسبه جمع حقوق هر صفحه مشاهده می‌کنید، مستقیما از lambda expressions برای تعریف یک anonymous delegate استفاده کنید.


نحوه اضافه کردن ستون ردیف
ستون ردیف، یک ستون محاسبه شده (calculated field) است:

grid.Column(header: "#",
style: "text-align-center-col",
format: @<text>@(rowIndex++)</text>),

نیازی نیست حتما یک grid.Column، به فیلدی در کلاس کارمند اشاره کند. مقدار سفارشی آن را به کمک پارامتر format تعیین خواهیم کرد. هر بار که قرار است یک ردیف رندر شود، یکبار این پارامتر فراخوانی خواهد شد. فرمول محاسبه rowIndex ابتدای صفحه را نیز پیشتر ملاحظه نمودید.


نحوه اضافه کردن ستون سفارشی تصاویر کارمندها
ستون تصویر کارمندها نیز مستقیما در کلاس کارمند تعریف نشده است. بنابراین می‌توان آن‌را با مقدار دهی صحیح پارامتر format ایجاد کرد:

grid.Column(header: "Image",
style: "text-align-center-col",
format: @<text><img alt="@item.Id" src="@Url.Content("~/images/" + @item.Id + ".jpg")" /></text>),


در این مثال، تصاویر کارمندها در پوشه images واقع در ریشه سایت، قرار دارند. به همین جهت از متد Url.Content برای مقدار دهی صحیح آن استفاده کردیم. به علاوه در اینجا @item.Id به Id رکورد در حال رندر اشاره می‌کند.


نحوه تبدیل تاریخ‌ها به تاریخ شمسی
در ادامه بازهم به کمک پارامتر format، یک وهله از شیء dynamic اشاره کننده به رکورد در حال رندر را دریافت می‌کنیم. سپس فرصت خواهیم داشت تا بر این اساس، فرمول نویسی کنیم. دست آخر هم رشته مورد نظر نهایی را بازگشت می‌دهیم:

grid.Column(columnName: "AddDate", header: "Start",
style: "text-align-center-col",
format: item =>
{
int ym = item.AddDate.Year;
int mm = item.AddDate.Month;
int dm = item.AddDate.Day;
var persianCalendar = new PersianCalendar();
int ys = persianCalendar.GetYear(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ms = persianCalendar.GetMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
int ds = persianCalendar.GetDayOfMonth(new DateTime(ym, mm, dm, new GregorianCalendar()));
return ys + "/" + ms.ToString("00") + "/" + ds.ToString("00");
}),


اضافه کردن ستون سفارشی مالیات
در کلاس کارمند، خاصیت حقوق وجود دارد اما مالیات خیر. با توجه به آن می‌توانیم به کمک پارامتر format، به اطلاعات شیء dynamic در حال رندر دسترسی داشته باشیم. بنابراین به اطلاعات حقوق دسترسی داریم و سپس با کمی فرمول نویسی، مقدار نهایی مورد نظر را بازگشت خواهیم داد. همچنین در اینجا می‌توان نحوه بازگشت مقدار حقوق را به صورت رشته‌ای حاوی جدا کننده‌های سه رقمی نیز مشاهده کرد:

grid.Column(columnName: "Salary", header: "Salary",
format: item =>
{
salaryPageSum += item.Salary;
return string.Format("${0:n0}", item.Salary);
},
style: "text-align-center-col"),
grid.Column(header: "Tax", canSort: true,
format: item =>
{
var tax = item.Salary * 0.2;
taxPageSum += tax;
return string.Format("${0:n0}", tax);
}),


اضافه کردن گردید‌های تو در تو
متد Grid.GetHtml، یک رشته را بر می‌گرداند. بنابراین در هر چند سطح که نیاز باشد می‌توان یک گرید را بر اساس اطلاعات دردسترس رندر کرد و سپس بازگشت داد:

grid.Column(header: "Projects", columnName: "Projects",
style: "text-align-center-col",
format: item =>
{
var subGrid = new WebGrid(
source: item.Projects,
canPage: false,
canSort: false
);
return subGrid.GetHtml(
htmlAttributes: new { id = "MySubGrid" },
tableStyle: "webgrid",
headerStyle: "webgrid-header",
footerStyle: "webgrid-footer",
alternatingRowStyle: "webgrid-alternating-row",
selectedRowStyle: "webgrid-selected-row",
rowStyle: "webgrid-row-style"
);
}),


در اینجا کار اصلی از طریق پارامتر format شروع می‌شود. سپس به کمک item.Projects به لیست پروژه‌های هر کارمند دسترسی خواهیم داشت. بر این اساس یک گرید جدید را تولید کرد و سپس رشته معادل با آن را به کمک متد subGrid.GetHtml دریافت و بازگشت می‌دهیم. این رشته در سلول جاری درج خواهد شد. به نوعی یک گزارش master detail یا sub report را تولید کرده‌ایم.


اضافه کردن دکمه‌های ویرایش، حذف و انتخاب
هر سه دکمه ویرایش، حذف و انتخاب در ستون‌هایی سفارشی قرار خواهند گرفت. بنابراین مقدار دهی header و format متد grid.Column کفایت می‌کند:

grid.Column(header: "",
style: "text-align-center-col",
format: item => @Html.ActionLink(linkText: "Edit", actionName: "Edit",
controllerName: "Home", routeValues: new { id = item.Id },
htmlAttributes: null)),
grid.Column(header: "",
format: @<form action="/Home/Delete/@item.Id" method="post"><input type="submit"
onclick="return confirm('Do you want to delete this record?');"
value="Delete"/></form>),
grid.Column(header: "", format: item => item.GetSelectLink("Select"))


نکته جدیدی که در اینجا وجود دارد متد item.GetSelectLink می‌باشد. این متد جزو متدهای توکار گرید است و کار آن بازگشت دادن شیء grid.SelectedRow می‌باشد. این شیء پویا، حاوی اطلاعات رکورد انتخاب شده است. برای مثال اگر نیاز باشد این اطلاعات به صفحه‌ای ارسال شود، می‌توان از روش زیر استفاده کرد:

@if (@grid.HasSelection)
{
@RenderPage("~/views/path/_partial_view.cshtml", new { Employee = grid.SelectedRow })
}


نمایش برچسب‌های صفحه x از n و رکوردهای x تا y از z
در یک گزارش خوب باید مشخص باشد که صفحه جاری، کدامین صفحه از چه تعداد صفحه کلی است. یا رکوردهای صفحه جاری چه بازه‌ای از تعداد رکوردهای کلی را تشکیل می‌دهند. برای این منظور چند متد کمکی به نام‌های WebGridPageFirstItem و WebGridPageLastItem تهیه شده‌اند که آن‌ها را در ابتدای View ارائه شده، مشاهده نمودید:

<strong>Page:</strong> @(grid.PageIndex + 1) / @grid.PageCount, 
<strong>Records:</strong> @WebGridPageFirstItem(@grid) - @WebGridPageLastItem(@grid) of @grid.TotalRowCount

نمایش جمع ستون‌های حقوق و مالیات در هر صفحه
گرید توکار همراه با ASP.NET MVC در این مورد راه حلی را ارائه نمی‌دهد. بنابراین باید اندکی دست به ابتکار زد. مثلا:

@section script{
<script type="text/javascript">
$(function () {
$('#MyGrid tbody:first').append(
'<tr class="total-row"><td></td>\
<td></td><td></td><td></td>\
<td><strong>Total:</strong></td>\
<td>@string.Format("${0:n0}", @salaryPageSum)</td>\
<td>@string.Format("${0:n0}", @taxPageSum)</td>\
<td></td><td></td><td></td></tr>');
});
</script>
}

در این مثال به کمک jQuery با توجه به اینکه id گرید ما MyGrid است، یک ردیف سفارشی که همان جمع محاسبه شده است، به tbody جدول نهایی تولیدی اضافه می‌شود. از tbody:first هم در اینجا استفاده شده است تا ردیف اضافه شده به گریدهای تو در تو اعمال نشود.
سپس فایل Views\Shared\_Layout.cshtml را گشوده و از section تعریف شده، برای مقدار دهی master page سایت، استفاده نمائید:

<head>
<title>@ViewBag.Title</title>
<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
@RenderSection("script", required: false)
</head>

مطالب
ایجاد فرم جستجوی پویا با استفاده از Expression ها
در مواردی نیاز است کاربر را جهت انتخاب فیلدهای مورد جستجو آزاد نگه داریم. برای نمونه جستجویی را در نظر بگیرید که کاربر قصد دارد: "دانش آموزانی که نام آنها برابر علی است و شماره دانش آموزی آنها از 100 کمتر است" را پیدا کند در شرایطی که فیلدهای نام و شماره دانش آموزی و عمل گر کوچک‌تر را خود کاربر به دلخواه برگزیرده.
روش‌های زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گام‌های ایجاد یک ساختار پایه برای این نوع فرم‌ها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روش‌ها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخاب‌های کاربرمی باشد.
1-  برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد می‌کنیم. سپس ساختار این سلوشن را بصورت زیر شکل می‌دهیم.


در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2-  کار را از پروژه مدل آغاز می‌کنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد می‌کنیم:
namespace DynamicSearch.Model
{
    public class Person
    {
        public Person(string name, string family, string fatherName)
        {
            Name = name;
            Family = family;
            FatherName = fatherName;
        }

        public string Name { get; set; }
        public string Family { get; set; }
        public string FatherName { get; set; }
    }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DynamicSearch.Model
{
    public class Teacher : Person
    {
        public Teacher(int id, string name, string family, string fatherName)
            : base(name, family, fatherName)
        {
            ID = id;
        }

        public int ID { get; set; }

        public override string ToString()
        {
            return string.Format("Name: {0}, Family: {1}", Name, Family);
        }
    }
}

namespace DynamicSearch.Model
{
    public class Student : Person
    {
        public Student(int stdId, Teacher teacher, string name, string family, string fatherName)
            : base(name, family, fatherName)
        {
            StdID = stdId;
            Teacher = teacher;
        }

        public int StdID { get; set; }
        public Teacher Teacher { get; set; }
    }
}
3- در پروژه سرویس یک کلاس بصورت زیر ایجاد می‌کنیم:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicSearch.Model;

namespace DynamicSearch.Service
{
    public class StudentService
    {
        public IList<Student> GetStudents()
        {
            return new List<Student>
                {
                    new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"),
                    new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"),
                };
        }
    }
}
4- تا اینجا تمامی داده‌ها صرفا برای نمونه بود. در این مرحله ساخت اساس جستجو گر پویا را شرح می‌دهیم.
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)

برای ذخیره المان‌های انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد می‌کنیم (همان سه جزء بالا):
using System;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public class AndOr
    {
        public AndOr(string name, string title,Func<Expression,Expression,Expression> func)
        {
            Title = title;
            Func = func;
            Name = name;
        }

        public string Title { get; set; }
        public Func<Expression, Expression, Expression> Func { get; set; }
        public string Name { get; set; }
    }
}

using System;

namespace DynamicSearch.ViewModel.Base
{
    public class Feild : IEquatable<Feild>
    {
        public Feild(string title, Type type, string name)
        {
            Title = title;
            Type = type;
            Name = name;
        }

        public Type Type { get; set; }
        public string Name { get; set; }
        public string Title { get; set; }
        public bool Equals(Feild other)
        {
            return other.Title == Title;
        }
    }
}

using System;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public class Operator
    {
        public enum TypesToApply
        {
            String,
            Numeric,
            Both
        }

        public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply)
        {
            Title = title;
            Func = func;
            TypeToApply = typeToApply;
        }

        public string Title { get; set; }
        public Func<Expression, Expression, Expression> Func { get; set; }
        public TypesToApply TypeToApply { get; set; }
    }
}
توسط کلاس زیر یک سری اعمال متداول را پیاده سازی کرده ایم و پیاده سازی اضافات را بعهده کلاس‌های ارث برنده از این کلاس گذاشته ایم:

using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;

namespace DynamicSearch.ViewModel.Base
{
    public abstract class SearchFilterBase<T> : BaseViewModel
    {
        protected SearchFilterBase()
        {
            var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String);
            var notContainOp = new Operator("شامل نباشد", (expression, expression1) =>
            {
                var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1);
                return Expression.Not(contain);
            }, Operator.TypesToApply.String);
            var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both);
            var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both);
            var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric);
            var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric);
            var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric);
            var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric);

            Operators = new ObservableCollection<Operator>
                {
                      equalOp, 
                      notEqualOp,
                      containOp,
                      notContainOp,
                      lessThanOp,
                      greaterThanOp,
                      lessThanOrEqual,
                      greaterThanOrEqual,
                };


            SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress");
            SelectedFeild = Feilds.FirstOrDefault();
            SelectedOperator = Operators.FirstOrDefault(a => a.Title == "=");
        }

        public abstract IQueryable<T> GetQuarable();

        public virtual ObservableCollection<AndOr> AndOrs
        {
            get
            {
                return new ObservableCollection<AndOr>
                    {
                        new AndOr("And","و", Expression.AndAlso), 
                        new AndOr("Or","یا",Expression.OrElse),
                        new AndOr("Suppress","نادیده",(expression, expression1) => expression),
                    };
            }
        }
        public virtual ObservableCollection<Operator> Operators
        {
            get { return _operators; }
            set { _operators = value; NotifyPropertyChanged("Operators"); }
        }
        public abstract ObservableCollection<Feild> Feilds { get; }

        public bool IsOtherFilters
        {
            get { return _isOtherFilters; }
            set { _isOtherFilters = value; }
        }
        public string SearchValue
        {
            get { return _searchValue; }
            set { _searchValue = value; NotifyPropertyChanged("SearchValue"); }
        }
        public AndOr SelectedAndOr
        {
            get { return _selectedAndOr; }
            set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); }
        }
        public Operator SelectedOperator
        {
            get { return _selectedOperator; }
            set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); }
        }
        public Feild SelectedFeild
        {
            get { return _selectedFeild; }
            set
            {
                Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric));
                if (SelectedOperator == null)
                {
                    SelectedOperator = Operators.FirstOrDefault(a => a.Title == "=");
                }

                NotifyPropertyChanged("SelectedOperator");
                NotifyPropertyChanged("SelectedFeild");
                _selectedFeild = value;
                NotifyPropertyChanged("SelectedFeildHasSetted");
            }
        }
        public bool SelectedFeildHasSetted
        {
            get
            {
                return SelectedFeild != null &&
                       (SelectedAndOr.Name != "Suppress" || !IsOtherFilters);
            }
        }

        private ObservableCollection<Operator> _operators;
        private Feild _selectedFeild;
        private Operator _selectedOperator;
        private AndOr _selectedAndOr;
        private string _searchValue;
        private bool _isOtherFilters = true;
    }
}
توضیحات: در این ویو مدل پایه سه لیست تعریف شده که برای دو تای آنها پیاده سازی پیش فرضی در همین کلاس دیده شده ولی برای لیست فیلدها پیاده سازی به کلاس ارث برنده واگذار شده است.

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using AutoMapper;

namespace DynamicSearch.ViewModel.Base
{
  public static  class ExpressionExtensions
    {
        public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities)
        {
            return entities.Provider.CreateQuery<T>(whereCallExpression).ToList();
        }

        public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities)
        {
            var whereCallExpression = Expression.Call(
                typeof(Queryable),
                "Where",
                new[] { entities.ElementType },
                entities.Expression,
                Expression.Lambda<Func<T, bool>>(condition, new[] { pe }));
            return whereCallExpression;
        }

        public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right)
        {
            var typeOfNullable = type;
            typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable;
            left = null;

            var typeMethodInfos = typeOfNullable.GetMethods();
            var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1);

            var propertyInfos = typeof(T).GetProperties();
            if (propertyName.Contains("."))
            {
                left = CreateComplexTypeExpression(propertyName, propertyInfos, pe);
            }
            else
            {
                var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName);
                if (propertyInfo != null) left = Expression.Property(pe, propertyInfo);
            }

            if (left != null) left = Expression.Convert(left, typeOfNullable);

            if (parseMethodInfo != null)
            {
                var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue });
                right = Expression.Constant(invoke, typeOfNullable);
            }
            else
            {
                //type is string
                right = Expression.Constant(searchValue.ToLower());
                var methods = typeof(string).GetMethods();
                var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any());
                if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault);
            }
        }

        public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe)
        {
            Expression ex = null;
            var infos = searchFilter.Split('.');
            var enumerable = propertyInfos.ToList();
            for (var index = 0; index < infos.Length - 1; index++)
            {
                var propertyInfo = infos[index];
                var nextPropertyInfo = infos[index + 1];
                if (propertyInfos == null) continue;
                var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo);
                if (propertyInfo2 == null) continue;
                var val = Expression.Property(pe, propertyInfo2);
                var propertyInfos3 = propertyInfo2.PropertyType.GetProperties();
                var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo);
                if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3);
            }

            return ex;
        }

        public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right)
        {
            return func.Invoke(left, right);
        }

        public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex)
        {
            if (!isFirst)
            {
                return func.Invoke(expression, ex);
            }

            expression = ex;
            return expression;
        }
    }
}
5- ایجاد کلاس فیلتر جهت معرفی فیلدها و معرفی منبع داده و ویو مدلی ارث برنده از کلاس‌های پایه ساختار، جهت ایجاد فرم نمونه:

using System.Collections.ObjectModel;
using System.Linq;
using DynamicSearch.Model;
using DynamicSearch.Service;
using DynamicSearch.ViewModel.Base;

namespace DynamicSearch.ViewModel
{
    public class StudentSearchFilter : SearchFilterBase<Student>
    {
        public override ObservableCollection<Feild> Feilds
        {
            get
            {
                return new ObservableCollection<Feild>
                    {
                        new Feild("نام دانش آموز",typeof(string),"Name"), 
                         new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"),
                        new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"),
                        new Feild("شماره دانش آموزی",typeof(int),"StdID"),
                    };
            }
        }

        public override IQueryable<Student> GetQuarable()
        {
            return new StudentService().GetStudents().AsQueryable();
        }
    }
}
6- ایجاد ویو نمونه:

در نهایت زمل فایل موجود در پروژه ویو:

<Window x:Class="DynamicSearch.View.MainWindow"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel"
        xmlns:view="clr-namespace:DynamicSearch.View"
        mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <Window.Resources>
        <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" />
        <view:VisibilityConverter x:Key="VisibilityConverter" />
    </Window.Resources>
    <Grid   DataContext="{StaticResource StudentSearchViewModel}">
        <WrapPanel Orientation="Vertical">
            <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}">
                <DataGrid.Columns>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox MinWidth="100"  DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}"
                                          SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn >
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100"   DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <ComboBox MinWidth="100"  DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}"
                                          SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}">
                                <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
                                <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>-->
                            </DataTemplate>
                        </DataGridTemplateColumn.CellTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>
            </DataGrid>

            <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/>
            <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/>
            <DataGrid ItemsSource="{Binding Results}">
                
            </DataGrid>
        </WrapPanel>
    </Grid>
</Window>
در این مقاله، هدف معرفی روند ایجاد یک جستجو گر پویا با قابلیت استفاده مجدد بالا بود و عمدا از توضیح جزء به جزء کدها صرف نظر شده. علت این امر وجود منابع بسیار راجب ابزارهای بکار رفته در این مقاله و سادگی کدهای نوشته شده توسط اینجانب می‌باشد.


برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلد‌ها در LINQ 
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx


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


پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدل‌ها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاری‌های مناسب تر

DynamicSearch.zip
 
مطالب
Pro Agile .NET Development With Scrum - قسمت دوم

داستان‌های کاربر

توسعه‌دهندگان، ویژگی‌های مورد نظر پروژه را با جمع‌آوری نیازمندی‌ها، در قالب داستانهای کاربر احصاء می‌کنند و به هرکدام متناسب با پیچیدگی‌اش امتیازی اختصاص می‌دهند. با لیستی از داستان‌های دارای ابعادی مشخص و بودجه و زمان مورد نیاز برای هرکدام، مشتریان قادر به این انتخابند که کدام ویژگی‌ها در تکرار (iteration) بعدی باقی بماند. مشخص‌کردن بودجه و زمان، یعنی تعیین حجم کاری که تیم توسعه برای انجام آن ویژگی، نیاز می‌داند. برآورد بودجۀ مورد نیاز تکرار اول به صورت تجربی خواهد بود و ممکن است این تخمین در ابتدا نادرست باشد؛ اما با شروع تکرار بعدی درست خواهد شد. در پایان هر تکرار، امتیازات به دست آمده از داستان‌های کامل شده را جمع کنید. مجموع این امتیازات، نشانگر سرعت شما خواهد بود. این سرعت شاخص خوبی جهت چگونگی بودجه‌بندی مرحلۀ بعد است. هنگامیکه امتیازات جمع‌آوری شده به حد مطلوبی رسید، «سرعت پیشرَوی»، شاخص مناسب دیگری برای بودجه‌بندی است که عبارت است از متوسط سرعت سه تکرار آخر.

با این کار شما به دیدگاه مناسبی از فاز برنامه‌ریزی دست پیدا می‌کنید. حال اجاز دهید نگاه دقیق‌تری به شیوه‌های برنامه‌ریزی داشته باشیم.

برنامه‌ریزی (planning game) دو فاز دارد: فاز شناسایی و فاز برنامه‌ریزی. در فاز شناسایی، توسعه‌دهندگان و مشتریان را دور هم جمع می‌کنند تا دربارۀ نیازمندیهای سیستم در حال طراحی، گفتگو کنند. به خاطر داشته باشید که این کار تا وقتی انجام می‌شود که به ویژگی‌هایی (features) کافی برای شروع انجام کار برسیم و البته واضح است که چنین لیستی از ویژگی‌های احصاء شده، هرچقدر هم که تلاش شود، کامل نخواهد بود. مشتریان اغلب اوقات، خواسته‌ی خود را یا نمی‌دانند یا نمی‌توانند به خوبی توضیح دهند. بنابراین معمولاً این لیست به مرور تغییر می‌کند. در ضمن آنکه برخی ویژگیها دقیق‌تر می‌شود، مواردی نیز ممکن است به لیست افزوده شوند یا حتی می‌توان برخی ویژگی‌های نامربوط را از لیست حذف کرد. در مرحلۀ شناسایی، ویژگی‌ها به داستانهای کاربر تجزیه شد و ثبت می‌شوند.

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

به عنوان «نوع کاربر»، من می‌خواهم «یک فعل» تا «منفعتی برای کسب و کار» 

یا به صورت:

به منظور «یک دلیل» به عنوان «نقش کاربر» من می‌خواهم «یک فعل»

داستانهای کاربر معمولاً در جلسه‌ی گفتگو با مشتری بر روی کارت‌های راهنما نوشته شده و در آن از واژگان و ادبیاتی استفاده می‌شود که برای مشتری قابل فهم باشد. ممکن است چنین بیاندیشید که ثبت نیازمندی‌ها، خلاف مزیت‌های چابک‌سازی است؛ چرا که تولید نرم‌افزار کارآمد و چابک مبتنی بر مستندسازی گسترده و فراگیر خواهد بود. در واقع، داستان‌های کاربر به طور ساده فقط یادآورندۀ جزئیات بیشتری از گفتگوی انجام شده‌اند که به عمد به‌صورت کوتاه و دقیق نوشته شده‌اند. فهم دقیق‌تر جزئیات کار، مستلزم ارتباط بیشتر میان توسعه‌دهندگان و مشتری است. در واقع همسو با این اصل چابک که می‌گوید: «مؤثرترین و کارآمدترین شیوۀ انتقال اطلاعات در میان تیم توسعه و به خارج از آن، گفتگوی چهره به چهره است.»

هنگام احصاء ویژگی‌های پروژه تحت عنوان داستان‌های کاربری، از اصول INVEST (که پیش‌تر گفته شد) جهت کنترل مناسب بودن این داستانها استفاده کنید. شکل 2-3 مثالی از یک داستان کاربر را که توصیف‌کنندۀ ویژگی «افزودن یک بن تخفیف به سبد خرید» است، نشان می‌دهد. «تخفیف گرفتن»، یک منفعت کسب و کار است برای عامل (actor) اصلی، یعنی مشتری. «یک بن تخفیف به سبد بیفزا» نام فرآیند یا «use case» مربوط است.

 


از معیار پذیرش (acceptance criteria) نیز می‌توان در هنگام تولید داستان‌ها استفاده کرد. معیار پذیرش را می‌توان در پشت کارت داستان، آن طور که در شکل 3-3 نشان داده شده است، نوشت. استفاده از طرف مقابل کارت این اجازه را می‌دهد که اعضای تیم و مشتریان، اطلاعات خودشان را در یک جا جمع کنند.  


معیار پذیرش همچنین به تشخیص جزئیات بیشتر یا شناسایی وابستگی‌ها کمک می‌کند. مثلاً در شکل 3-3 تعریف «in date» چیست و چه چیزی حدود یک بن تخفیف را مشخص می‌کند؟ معمولاً باید حداقل سه معیار پذیرش وجود داشته باشد. در فصل بعد در یک مطالعۀ موردی، مطالب بیشتری را دربارۀ داستانهای کاربر خواهید آموخت.

هنگامیکه تیم و مشتریان حس‌کنند که حدود 75 درصد از ویژگی‌های اصلی احصاء شده است، توسعه‌دهندگان ابعاد داستان‌ها را تخمین زده و آنها را برای اولویت‌بندی توسط مشتری آماده می‌کنند.


تخمین 

شکی در آن نیست که تخمین‌زدن کار سختی است. تخمین‌زدن هم دانش است هم هنر. تخمین‌زدن در یک پروژۀ تازه شروع شده، بسیار سخت است زیرا مجهولات بسیاری در آن وجود دارد. 

یکی از روش‌های تخمین گروهی، روش «Planning Poker» نام دارد. در این روش همه‌ی اعضای فنی تیم، متشکل از توسعه‌دهندگان نرم‌افزار، تحلیل‌گران، متخصصان امنیت و زیرساخت، مشارکت می‌کنند. نقش مشتری در این حالت پاسخ‌گویی به سؤالات احتمالی اعضای تیم است تا ایشان بهتر بتوانند تخمین بزنند.

شیوۀ انجام کار به این صورت است که عضوی از تیم، یک داستان کاربر را برداشته و آن را برای تیم توضیح می‌دهد. تیم دربارۀ آن ویژگی با مشتری گفتگو کرده تا جزئیات بیشتری را دریابد. وقتی که تیم به درک خوبی از آن رسید، رأی‌گیری آغاز می‌شود. هر عضو تیم با یک کارت، از مجموعه‌ای ازکارتهایی با شماره‌های 0، 1 ، 2، 3، 5، 8، 13، 20، 40 و 100 رأی خود را اعلام می‌کند.

تیم باید از داستانی شروع کند که نسبتاً کوچک و ساده باشد. این داستان به عنوان مبنا انتخاب می‌شود. هر تخمین داستان کاربر، باید به نسبت این داستان کوچک انجام شود. اگر داستان مبنا به خوبی انتخاب نشود، بقیۀ تخمین‌ها نادرست خواهد بود.

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

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

داستان‌هایی که بیش از یک هفته کار نیاز داشته باشند با عنوان داستانهای حماسی (epic stories) شناخته می‌شوند و معمولاً برای تخمین بسیار بزرگ هستند. در واقع، این داستان‌ها به چند داستان کوچک‌تر که قابل فهم‌تر و به آسانی قابل تخمین باشند، تجزیه می‌شوند. این بدان معناست که ایجاد یک داستان کاربر از تعداد انبوهی ویژگی موجب کاهش کارآیی خواهد شد. 

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

استفاده از ابزار Planning Poker مزایای بسیاری دربردارد. دقت تخمین بالا می‌رود؛ زیرا مسأله از منظر تخصص‌های گوناگون مورد بررسی قرار گرفته است. همچنین به تیم کمک می‌کند که هم رأی شوند و گفتگو میان اعضاء را تسهیل می‌کند. پس از آنکه داستان‌ها تخمین زده شدند، مشتری و صاحب محصول با تیم توسعه در تولید چگونگی انتشار نسخه‌ها، همکاری می‌کنند.


برنامه انتشار 

اگرچه کدهای قابل ارسال، قابلیت انتشار در پایان هر تکرار را دارند، اما یک پروژه XP در چند سری منتشر شده است. یک نسخۀ منتشرشده، متشکل از تعداد مناسبی داستان برای عرضۀ ارزش کسب وکاری است که به کوچک نگه داشتن آن کمک می‌کند. بسیار مناسب است که یک موضوع یا هدف خاص را در ضمن هرنسخۀ انتشار، مد نظر قرار داد تا کمک کند که هر نسخۀ انتشار بر برخی ارزشهای کسب و کاری متمرکز شده و آن را هدایت کند. معمولاً یک نسخۀ انتشار، متشکل از چهار تکرار است؛ همانطور که در شکل 4-3 نشان داده شده است.

 


در برنامه‌ریزی نسخه‌های انتشار، طول یک تکرار نیز تعیین می‌شود که معمولاً بین دو تا چهار هفته است. مطابق تجربه، اگر محیط کار شما دچار بی‌نظمی و اختلالات دائمی است، می‌توانید دورۀ تکرار را به یک هفته محدود کنید.

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

اگر قصد دارید به صورت دقیق بر روی حلقۀ بازخورد متمرکز شوید، دوره‌ی تکرار یک هفته‌ای، مدل خوبی است. اما این مدل سربار زیادی را به دلیل ضرورت تقسیم داستانهای کاربر باید به بخش‌های کوچک‌تری تا آن اندازه که در یک دوره تکمیل شوند، بر پروژه تحمیل می‌کند. در ادامه خواهیم گفت که هر تکرار شامل برنامۀ ملاقات و بازبینی نیز هست.

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

مشتری با توجه به طول دورۀ تکرار و بودجۀ داستان آغازین، انتخاب می‌کند که کدام داستان در هنگام انتشار نسخۀ اوّل، در تکرار اوّل کامل شود. 

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

معمولاً باید به داستانهای کاربری‌ای که مستلزم ریسک بالا بوده یا دربرگیرندۀ مجهولات زیادی باشند، بیش از یک یا دو تکرار اختصاص داد. 


برنامۀ تکرار

مشتری داستان‌هایی را که می‌خواهد در تکرار باشند، انتخاب می‌کند. برای هر داستان کاربر، مجموعه‌ای از معیارهای پذیرش، تعریف شده است. همان طور که متوجه شده‌اید ما در هر فاز، وقت بیشتر و بیشتری را صرف جمع‌آوری جزئیات هر داستان کاربر کرده و بصورت عمیق‌تری در آن غور می‌کنیم. این کار مفید است، زیرا اگر یک داستان کاربر ایجاد شده در ابتدای پروژه، ممکن است بعداً به عنوان داستانی کم اهمیت یا غیر مهم دیده‌شود و بدون آنکه وقت خاصی برای آن صرف شده باشد، کنار گذاشته شود. اما اگر در ابتدای کار وقت زیادی صرف دقیق‌تر کردن داستان‌های کاربر شود و بعداً بعضی از آنها کنار گذاشته شوند، در واقع وقت تلف شده است. بنابراین دقیق‌تر کردن یک داستان در جایی که مورد نیاز است، باید اتفاق بیفتد. در سطح برنامۀ تکرار، مجموعه‌ای از معیارهای پذیرش را برای هر داستان کاربر تعریف می‌کنیم. معیار پذیرش به توسعه‌دهنده کمک می‌کند تا بداند که یک داستان کاربر به طور کامل انجام می‌شود. این معیارها به صورت مؤلفه‌هایی از بافرض/هنگامی که/درنتیجه، نوشته می‌شود. 

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

عنوان ویژگی: افزودن کالایی به سبد

به عنوان یک مشتری می‌خواهم بتوانم کالایی را به سبدم اضافه کنم؛ به نحوی که قادر باشم به خرید خود ادامه دهم.

سناریو: سبد  خالی

با فرض اینکه یک سبد خالی دارم، در نتیجه جمع تعداد کالایی که برای سفارش در سبد من وجود دارد، صفر است.

سناریو: افزودن یک کالا به سبد

با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه می‌کنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 1 می‌شود.

سناریو: افزودن کالاهایی به سبد

با فرض اینکه یک سبد خالی دارم، هنگامی که کالایی با شناسۀ 1 و کالایی با شناسۀ 2 به سبدم اضافه می‌کنم، در نتیجه جمع کالاهای قابل سفارش در سبدم 2 می‌شود.

سناریو: دو بار افزودن یک کالا

با فرض اینکه یک سبد خالی دارم هنگامی که کالایی با شناسۀ 1 به سبدم اضافه می‌کنم و هنگامی که کالایی با شناسۀ 1 را مجدداً به سبدم اضافه می‌کنم، در نتیجه تعداد کالاهای با شناسۀ 1 در سبد من باید 2 باشد.

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

با فرض اینکه یک سبد خالی دارم و کالایی با شناسۀ 2 در انبار وجود نداشته باشد، هنگامی که من کالایی با شناسۀ 2  را به سبد خودم اضافه می‌کنم، در نتیجه جمع تعداد کالای قابل سفارش در سبد من باید 0 باشد و به کاربر، موجود نبودن آن کالا را هشدار دهد.

یک آزمون پذیرش (acceptance) به زبان متعارف در قوانین کسب و کار نوشته می‌شود. در مثال سبد خرید، این سؤال پیش می‌آید که چگونه می‌توان یک محصول را از سبد کالا، حذف کرد و اگر یک جنس اکنون در انبار نیست و کاربر پیام هشدار دریافت کرده است، در ادامه چه اتفاقی باید بیفتد؟ سناریوها به تیم در کشف ملزومات کسب و کار و تصریح آن‌ها کمک می‌کند.

این سناریوها توسط توسعه‌دهنده به عنوان نقطۀ شروع آزمونهای واحد در توسعۀ آزمون محور و رفتار محور استفاده می‌شود. سناریوها همچنین در آزمودن معیارهای پذیرش به توسعه‌دهنده کمک کرده و توسعه‌دهنده و تست‌کننده را قادر می‌سازند که بر روی اتمام داستان اتفاق نظر داشته باشند.

بعد از آنکه سناریوهای معیار پذیرش تعیین شد، تیم توسعه، هر داستان را به تعدادی وظیفه تقسیم می‌کند و وظایف مرتبط به یک داستان، در تابلوی وظایف قرارگرفته و تیم توسعه تخمین‌های خود را در قالب یکی از واحدهای اندازه‌گیری، مثلاً نفرساعت  اعلام می‌کند. شکل 5-3 یک تابلوی وظیفه را نمایش می‌دهد.

به عنوان مثال وظایف می‌توانند شامل ایجاد طرح یک بانک اطلاعاتی برای یک داستان یا یکپارچه‌سازی آن با بخشی موجود در سیستم باشند. وظایف شامل مؤلفه‌های فنی مانند تهیۀ گزارش از زیرسیستم‌ها یا چارچوب مدیریت استثنائات نیز می‌باشد. اغلب این‌گونه وظایف نادیده‌گرفته می‌شود. یک داستان کاربر با وظایف گوناگونی گره خورده است. مثلاً:

داستان کاربر : به عنوان یک کاربر می‌خواهم بتوانیم یک کاربر را مدیریت کنم.

وظایف زیر از این داستان قابل استخراج است:

  • طرحی برای بانک اطلاعات جهت ذخیره‌سازی اطلاعات کاربر ایجاد کن.
  • یک کلاس کاربر، برای مدیریت کاربر از درون برنامه ایجاد کن.

هر عضو تیم می‌تواند بر روی هر وظیفه‌ای که بر روی تخته است، کار کند. هنگامیکه یک عضو گروه، وظیفه‌ای را برمی‌دارد، باید نشانی از خود روی کارت آن وظیفه قراردهد ( مثلاً حروف اوّل اسمش) تا بقیۀ افراد بدانند که وی بر روی آن وظیفه، مشغول به کار است. معمولاً اما نه همیشه، یک توسعه‌دهنده همۀ وظایف مربوط به یک داستان را برمی‌دارد. این کار بدین معناست که آن توسعه‌دهنده با پشتیبانی تیم، مسؤول اتمام آن کار است.

 

شکل 5-3. تختۀ وظایف نشان‌دهندۀ چگونگی پیشرفت پروژه

به محض اینکه یک تکرار آغاز شد، داستان‌هایی را که کار بر روی آنها شروع شده است، دیگر نمی‌توان تغییر داد. این مهم است که برنامۀ تکرار را در حین انجام آن، تغییر ندهید؛ زیرا این کار منجر به سوئیچنگ زمینه (context switching) می‌شود. برای توسعه‌دهندگان سوئیچینگ زمینه، هم به لحاظ زمانی و هم به لحاظ مالی، بسیار پرهزینه است.
به جای آنکه تلاش کنید در ضمن یک تکرار، تغییراتی را ایجاد کنید، مشخص کنید که آیا این کار اضافه، یا داستان اضافه را می‌توان تا تکرار بعدی به تعویق انداخت. مشتریان یا مدیران معمولاً می‌توانند چنین تعویقی را بپذیرند؛ زیرا این پذیرش مستلزم به تاخیر انداختن کار، مثلاً تا یک ماه دیگر نیست. اگر این کار جدید را که اضافه شده است، نمی‌توان به تعویق انداخت، باید ریسک خارج ساختن کدهای موجود و رفتن به سمت کدنویسی برای کارکرد جدید را به همراه تیم بررسی کرد. همچنین تیم باید بداند که اگر کار اضافه‌ای به یک دورۀ تکرار افزوده شد، بخشی از کارهای این دوره باید به تکرار بعدی موکول شوند. قاعدۀ کلی این است که اگر چیزهای جدیدی به کار وارد شد و تعویق آن ممکن نبود، باید کارهایی با همان ابعاد یا بزرگتر از تکرار، خارج شود. 
سرعت به ما نشان می‌دهد که تیم چه حجم کاری را در طول یک دوره کامل کرده است. از سرعت، در برنامه‌ریزی تکرارهای آتی استفاده می‌شود. برای درک چگونگی سرعت کار از نمودار burn-down استفاده می‌‌شود. یک نمودار burn-down (شکل 6-3) داستان‌های باقی‌ماندۀ یک پروژه و داستانهای تکمیل شده را در یک تکرار نمایش می‌دهد. سرعت در پایان هر تکرار محاسبه می‌شود و تعریف آن عبارت است از تعداد داستان‌های تکمیل شده در آخرین تکرار. بر اساس سرعت کنونی و تعداد داستان‌های باقی‌مانده، می‌توان تخمین زد که چقدر طول می‌کشد تا همه‌ی داستانها تکمیل شود. همانند آنچه در شکل 6-3 با خط چین نمایش داده شده است.
نمودار burn-down ابزار خوبی برای فهم آن است که آیا تیم می‌تواند پروژه را در زمان مقتضی به پایان برساند یا خیر و اگر نمی‌تواند، مدیر چگونه باید نسبت به آن تصمیم‌گیری کند. آیا افراد بیشتری باید به پروژه وارد شوند؟ آیا باید از ویژگی‌های مدنظر پروژه کاهش داد، یا باید زمان پایان کار را تغییر داد؟ 
 


در طول یک تکرار، هر روز باید گفتگوهایی سرپایی با حضور همۀ اعضای تیم انجام شود و مشکلاتی که ممکن است باعث به تأخیر افتادن ارائه کار شود، مورد بحث و بررسی قرار گیرد و همچنین تیم، لیست وظایف و تخته آن را به‌روز کرده تا پیشرفت یا موانع آن به وضوح قابل رؤیت باشند. 


با تشکر از آقای سید مجتبی حسینی
مطالب
پر کردن مقادیر Enum در کلاینت

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

میتوان در این گونه موارد از قابلیت‌های گوناگونی استفاده کرد که در اینجا ما با استفاده از یک فیلد مخفی و json مقدار را به کلاینت پاس میدهیم و در این مثال  در سمت کلاینت نیز دراپ دان را با این مقادیر پر میکنیم: 

    public enum PersistType
    {
        Persistable = 1,
        NotPersist = 2,
        AlwaysPersist = 3
    }

لیست را باید قبل از پر کردن در فیلد مخفی به json بصورت serialize شده تبدیل کرد، برای این منظور از JavaScriptSerializer موجود در اسمبلی‌های دات نت در متد زیر استفاده شده:

        public static string ConvertEnumToJavascript(Type t)
        {
            if (!t.IsEnum) throw new Exception("Type must be an enumeration");

            var values = System.Enum.GetValues(t);
            var dict = new Dictionary<int, string>();

            foreach (object obj in values)
            {
                string name = System.Enum.GetName(t, obj);
                dict.Add(Convert.ToInt32(System.Enum.Format(t, obj, "D")), name);
            }

            return new JavaScriptSerializer().Serialize(dict);
        }

با توجه به اینکه در سمت کلاینت مقدار json ذخیره شده در فیلد مخفی را میتوان به صورت آبجکت برخورد کرد پس یک متد در سمت کلاینت این آبجکت را در loop قراد داده و درمتغییری در فایل جاوا اسکریپت نگهداری میکنیم:

var Enum_PersistType = null;

function SetEnumTypes() {
    Enum_PersistType = JSON.parse($('#hfJsonEnum_PersistType').val());
}

و در هر قسمت که نیاز به مقدار enum بود با توجه به ایندکس مقدار را برای نمایش ازاین متغییر بیرون میکشیم:

function GetPersistTypeTitle_Concept(enumId) {
    return Enum_PersistType[enumId];
}

برای مثال در dropdown در سمت کلاینت این نوع استفاده شده و در حالتی از صفحه فقط برای نمایش عنوان آن احتیاج به دریافت آن از سمت سرور باشد میتوان از این روش کمک گرفت

و یا در سمت javascript میتوان با استفاده از jQuery مقادیر متغییر را در dropdown پر کرد.

function FillDropdown() {
    $("#ddlPersistType").html("");
    $.each(Enum_PersistType, function (key, value) {
        $("#ddlPersistType").append($("<option></option>").val(key).html(value));
    });
}

FillDropdownListOnClient.zip