مطالب
انجام اعمال ریاضی بر روی Generics
کامپایلر سی‌شارپ اگر نتواند نوع‌های عملوندها را در حین بکارگیری عملگرها تشخیص دهد، اجازه‌ی استفاده از عملگر را نخواهد داد و کار کامپایل، با یک خطا خاتمه می‌یابد. برای نمونه مثال زیر را در نظر بگیرید:
    public interface ICalculator<T>
    {
        T Add(T operand1, T operand2);
    }

    public class Calculator<T> : ICalculator<T>
    {
        public T Add(T operand1, T operand2)
        {
            return operand1 + operand2;
        }
    }
در اینجا چون کامپایلر نمی‌داند که عملگر + بر روی چه نوع‌هایی قرار است اعمال شود (به علت جنریک تعریف شدن این نوع‌ها و مشخص نبودن اینکه آیا این نوع، اصلا عملگر + دارد یا خیر)، با صدور خطای زیر، عملیات کامپایل را متوقف می‌کند:
 Operator '+' cannot be applied to operands of type 'T' and 'T'
برای حل این مساله، چندین روش مطرح شده‌است که در ادامه تعدادی از آن‌ها را مرور خواهیم کرد.


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

این راه حلی است که توسط اعضای تیم سی‌شارپ در روزهای ابتدایی معرفی جنریک‌ها مطرح شده‌است. فرض کنید می‌خواهیم لیستی از جنریک‌ها را با هم جمع بزنیم:
    public class Calculator2<T>
    {
        public T Sum(List<T> list)
        {
            T sum = 0;
            for (int i = 0; i < list.Count; i++)
                sum += list[i];
            return sum;
        }
    }
این کد نیز قابل کامپایل نبوده و امکان اعمال عملگر + بر روی نوع ناشناخته‌ی T میسر نیست.
    public interface ICalculator<T>
    {
        T Add(T operand1, T operand2);
    }

    public class Int32Calculator : ICalculator<int>
    {
        public int Add(int operand1, int operand2)
        {
            return operand1 + operand2;
        }
    }

    public class AlgorithmLibrary<T> where T : new() 
    {
        private readonly ICalculator<T> _calculator;
        public AlgorithmLibrary(ICalculator<T> calculator)
        {
            _calculator = calculator;
        }

        public T Sum(List<T> items)
        {
            var sum = new T();
            for (var i = 0; i < items.Count; i++)
            {
                sum = _calculator.Add(sum, items[i]);
            }
            return sum;
        }
    }
در راه حل ارائه شده، یک اینترفیس عمومی که متد جمع را تعریف کرده‌است، مشاهده می‌کنیم. سپس این اینترفیس در سازنده‌ی کتابخانه‌ی الگوریتم‌‌های برنامه تزریق شده‌است. اکنون کدهای AlgorithmLibrary بدون مشکل کامپایل می‌شوند. هر زمان که نیاز به استفاده از آن بود، بر اساس نوع T، پیاده سازی خاصی را باید ارائه داد. برای مثال در اینجا Int32Calculator پیاده سازی نوع int را انجام داده‌است. برای استفاده از آن نیز خواهیم داشت:
 var result = new AlgorithmLibrary<int>(new Int32Calculator()).Sum(new List<int> { 1, 2, 3 });

البته این نوع پیاده سازی را که کار اصلی آن واگذاری عملیات جمع، به یک کلاس خارجی است، توسط Func نیز می‌توان خلاصه‌تر کرد:
    public class Algorithms<T> where T : new() 
    {
        public T Calculate(Func<T, T, T> add, IEnumerable<T> numbers)
        {
            var sum = new T();
            foreach (var number in numbers)
            {
                sum = add(sum, number);
            }
            return sum;
        }
    }
استفاده از Action و Func نیز یکی دیگر از روش‌های تزریق وابستگی‌ها است که در اینجا بکار گرفته شده‌است. برای استفاده از آن خواهیم داشت:
 var result = new Algorithms<int>().Calculate((a, b) => a + b, new[] { 1, 2, 3 });
آرگومان اول روش جمع زدن را مشخص می‌کند و آرگومان دوم، لیستی است که باید اعضای آن جمع زده شوند.


روش دوم: استفاده از واژه‌ی کلیدی dynamic

با استفاده از واژه‌ی کلیدی dynamic می‌توان بررسی نوع داده‌ها را به زمان اجرا موکول کرد. به این ترتیب دیگر کامپایلر مشکلی با کامپایل قطعه کد ذیل نخواهد داشت:
    public class Calculator<T> : ICalculator<T>
    {
        public T Add(T operand1, T operand2)
        {
            return (dynamic)operand1 + operand2;
        }
    }
و مثال زیر نیز به خوبی کار می‌کند:
 var test = new Calculator<int>().Add(1, 2);
البته بدیهی است که نوع تعریف شده در اینجا باید دارای عملگر + باشد. در غیر اینصورت در زمان اجرا برنامه با یک خطا خاتمه خواهد یافت.
روش فوق نسبت به حالتی که بر اساس نوع T تصمیم‌گیری شود و از عملگر + متناظری استفاده گردد، خوانایی بهتری دارد:
public T Add(T t1, T t2)
{
    if (typeof(T) == typeof(double))
    {
        var d1 = (double)t1;
        var d2 = (double)t2;
        return (T)(d1 + d2);
    }
    else if (typeof(T) == typeof(int)){
        var i1 = (int)t1;
        var i2 = (int)t2;
        return (T)(i1 + i2);
    }
    else ...
}


روش سوم: استفاده از Expression Trees

روش زیر بسیار شبیه است به حالتیکه از Func در روش اول استفاده شد. در اینجا این Func به صورت پویا تولید و سپس صدا زده می‌شود:
using System;
using System.Linq.Expressions;

namespace GenericsArithmetic
{
    public class Solution3
    {
        public T Add<T>(T a, T b)
        {
            var paramA = Expression.Parameter(typeof(T), "a");
            var paramB = Expression.Parameter(typeof(T), "b");

            var body = Expression.Add(paramA, paramB);
            var add = Expression.Lambda<Func<T, T, T>>(body, paramA, paramB).Compile();
            return add(a, b);
        }
    }
}
البته این مثال، یک مثال ابتدایی در این مورد است. بر همین مبنا و ایده، یک کتابخانه‌ی با کارآیی بالا، تحت عنوان Generic Operators که جزو Misc utils می‌باشد، تهیه شده‌است.
به کمک کتابخانه‌ی Generic Operators، کدهای جمع زدن اعضای یک لیست جنریک به صورت ذیل خلاصه می‌شوند:
public static T Sum<T>(this IEnumerable<T> source)
{
    T sum = Operator<T>.Zero;
    foreach (T value in source)
    {
            sum = Operator.Add(sum, value);
    }
    return sum;
}
مطالب
استفاده از کتابخانه DotNetZip و CPUهای چند هسته‌ای
هرچند از دات نت 4 و نیم به بعد، الگوریتم Zip به صورت توکار پشتیبانی می‌شود، اما برای نگارش‌های پایین‌تر، کتابخانه DotNetZip جزو پرکاربردترین‌‌ها در این زمینه است.
از همین کتابخانه مدتی در یک سرور معمولی بدون مشکل استفاده کرده بودم تا اینکه پس از ارتقاء به سرور جدید با خراب بودن فایل‌های Zip حاصل مواجه شدم. پس از بررسی مشخص شد که این کتابخانه با CPUهای چند هسته‌ای مشکل دارد و باید این نوع پردازش موازی را در آن خاموش کرد:
using (var zf = new ZipFile())
{
   zf.UseUnicodeAsNecessary = true;
   zf.ParallelDeflateThreshold = -1;
   zf.Comment = "….";
   zf.AddFile(path, "programName");
   zf.Save(whereToSave);
}
همانطور که مشاهده می‌کنید، برای غیرفعال سازی پردازش موازی، فقط کافی است پارامتر ParallelDeflateThreshold با منهای یک مقدار دهی شود.
همچنین اگر نام فایل‌های شما فارسی است نیاز است UseUnicodeAsNecessary نیز به true تنظیم گردد.

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



مدل برنامه

در اینجا قصد داریم لیستی را با ساختار کلاس Product در اختیار Kendo UI گرید قرار دهیم:
namespace KendoUI03.Models
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}


پیشنیاز تامین داده مخصوص Kendo UI Grid

برای ارائه اطلاعات مخصوص Kendo UI Grid، ابتدا باید درنظر داشت که این گرید، درخواست‌های صفحه بندی خود را با فرمت ذیل ارسال می‌کند. همانطور که مشاهده می‌کنید، صرفا یک کوئری استرینگ با فرمت JSON را دریافت خواهیم کرد:
 /api/products?{"take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
سپس این گرید نیاز به سه فیلد، در خروجی JSON نهایی خواهد داشت:
{
"Data":
[
{"Id":1500,"Name":"نام 1500","Price":2499.0,"IsAvailable":false},
{"Id":1499,"Name":"نام 1499","Price":2498.0,"IsAvailable":true}
],
"Total":1500,
"Aggregates":null
}
فیلد Data که رکوردهای گرید را تامین می‌کنند. فیلد Total که بیانگر تعداد کل رکوردها است و Aggregates که برای گروه بندی بکار می‌رود.

می‌توان برای تمام این‌ها، کلاس و Parser تهیه کرد و یا ... پروژه‌ی سورس بازی به نام  Kendo.DynamicLinq نیز چنین کاری را میسر می‌سازد که در ادامه از آن استفاده خواهیم کرد. برای نصب آن تنها کافی است دستور ذیل را صادر کنید:
 PM> Install-Package Kendo.DynamicLinq
Kendo.DynamicLinq به صورت خودکار System.Linq.Dynamic را نیز نصب می‌کند که از آن جهت صفحه بندی پویا استفاده خواهد شد.


تامین کننده‌ی داده سمت سرور

همانند مطلب کار با Kendo UI DataSource ، یک ASP.NET Web API Controller جدید را به پروژه اضافه کنید و همچنین مسیریابی‌های مخصوص آن‌را به فایل global.asax.cs نیز اضافه نمائید.
using System.Linq;
using System.Net.Http;
using System.Web.Http;
using Kendo.DynamicLinq;
using KendoUI03.Models;
using Newtonsoft.Json;

namespace KendoUI03.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);
        }
    }
}
تمام کدهای این کنترلر همین چند سطر فوق هستند. با توجه به ساختار کوئری استرینگی که در ابتدای بحث عنوان شد، نیاز است آن‌را توسط کتابخانه‌ی JSON.NET تبدیل به یک نمونه از DataSourceRequest نمائیم. این کلاس در Kendo.DynamicLinq تعریف شده‌است و حاوی اطلاعاتی مانند take و skip کوئری LINQ نهایی است.
ProductDataSource.LatestProducts صرفا یک لیست جنریک تهیه شده از کلاس Product است. در نهایت با استفاده از متد الحاقی جدید ToDataSourceResult، به صورت خودکار مباحث صفحه بندی سمت سرور به همراه مرتب سازی اطلاعات، صورت گرفته و اطلاعات نهایی با فرمت DataSourceResult بازگشت داده می‌شود. DataSourceResult نیز در Kendo.DynamicLinq تعریف شده و سه فیلد یاد شده‌ی Data، Total و Aggregates را تولید می‌کند.

تا اینجا کارهای سمت سرور این مثال به پایان می‌رسد.


تهیه View نمایش اطلاعات ارسالی از سمت سرور

اعمال مباحث بومی سازی
<head>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Language" content="fa" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <!--شیوه نامه‌ی مخصوص راست به چپ سازی-->
    <link href="styles/kendo.rtl.min.css" rel="stylesheet" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>

    <!--محل سفارشی سازی پیام‌ها و مسایل بومی-->
    <script src="js/cultures/kendo.culture.fa-IR.js" type="text/javascript"></script>
    <script src="js/cultures/kendo.culture.fa.js" type="text/javascript"></script>
    <script src="js/messages/kendo.messages.en-US.js" type="text/javascript"></script>

    <style type="text/css">
        body {
            font-family: tahoma;
            font-size: 9pt;
        }
    </style>

    <script type="text/javascript">
        // جهت استفاده از فایل: kendo.culture.fa-IR.js
        kendo.culture("fa-IR");
    </script>
</head>
- در اینجا چند فایل js و css جدید اضافه شده‌اند. فایل kendo.rtl.min.css جهت تامین مباحث RTL توکار Kendo UI کاربرد دارد.
- سپس سه فایل kendo.culture.fa-IR.js، kendo.culture.fa.js و kendo.messages.en-US.js نیز اضافه شده‌اند. فایل‌های fa و fa-Ir آن هر چند به ظاهر برای ایران طراحی شده‌اند، اما نام ماه‌های موجود در آن عربی است که نیاز به ویرایش دارد. به همین جهت به سورس این فایل‌ها، جهت ویرایش نهایی نیاز خواهد بود که در پوشه‌ی src\js\cultures مجموعه‌ی اصلی Kendo UI موجود هستند (ر.ک. فایل پیوست).
- فایل kendo.messages.en-US.js حاوی تمام پیام‌های مرتبط با Kendo UI است. برای مثال«رکوردهای 10 تا 15 از 1000 ردیف» را در اینجا می‌توانید به فارسی ترجمه کنید.
- متد kendo.culture کار مشخص سازی فرهنگ بومی برنامه را به عهده دارد. برای مثال در اینجا به fa-IR تنظیم شده‌است. این مورد سبب خواهد شد تا از فایل kendo.culture.fa-IR.js استفاده گردد. اگر مقدار آن‌را به fa تنظیم کنید، از فایل kendo.culture.fa.js کمک گرفته خواهد شد.

راست به چپ سازی گرید
تنها کاری که برای راست به چپ سازی Kendo UI Grid باید صورت گیرد، محصور سازی div آن در یک div با کلاس مساوی k-rtl است:
    <div class="k-rtl">
        <div id="report-grid"></div>
    </div>
k-rtl و تنظیمات آن در فایل kendo.rtl.min.css قرار دارند که در ابتدای head صفحه تعریف شده‌است.

تامین داده و نمایش گرید

در ادامه کدهای کامل DataSource و Kendo UI Grid را ملاحظه می‌کنید:
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    },
                    parameterMap: function (options) {
                        return kendo.stringify(options);
                    }
                },
                schema: {
                    data: "Data",
                    total: "Total",
                    model: {
                        fields: {
                            "Id": { type: "number" }, //تعیین نوع فیلد برای جستجوی پویا مهم است
                            "Name": { type: "string" },
                            "IsAvailable": { type: "boolean" },
                            "Price": { type: "number" }
                        }
                    }
                },
                error: function (e) {
                    alert(e.errorThrown);
                },
                pageSize: 10,
                sort: { field: "Id", dir: "desc" },
                serverPaging: true,
                serverFiltering: true,
                serverSorting: true
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                filterable: true,
                reorderable: true,
                columnMenu: true,
                columns: [
                    { field: "Id", title: "شماره", width: "130px" },
                    { field: "Name", title: "نام محصول" },
                    {
                        field: "IsAvailable", title: "موجود است",
                        template: '<input type="checkbox" #= IsAvailable ? checked="checked" : "" # disabled="disabled" ></input>'
                    },
                    { field: "Price", title: "قیمت", format: "{0:c}" }
                ]
            });
        });
    </script>
- با تعاریف مقدماتی Kendo UI DataSource پیشتر آشنا شده‌ایم و قسمت read آن جهت دریافت اطلاعات از سمت سرور کاربرد دارد.
- در اینجا ذکر contentType الزامی است. زیرا ASP.NET Web API بر این اساس است که تصمیم می‌گیرد، خروجی را به صورت JSON ارائه دهد یا XML.
- با استفاده از parameterMap، سبب خواهیم شد تا پارامترهای ارسالی به سرور، با فرمت صحیحی تبدیل به JSON شده و بدون مشکل به سرور ارسال گردند.
- در قسمت schema باید نام فیلدهای موجود در DataSourceResult دقیقا مشخص شوند تا گرید بداند که data را باید از چه فیلدی استخراج کند و تعداد کل ردیف‌ها در کدام فیلد قرار گرفته‌است.
- نحوه‌ی تعریف model را نیز در اینجا ملاحظه می‌کنید. ذکر نوع فیلدها در اینجا بسیار مهم است و اگر قید نشوند، در حین جستجوی پویا به مشکل برخواهیم خورد. زیرا پیش فرض نوع تمام فیلدها string است و در این حالت نمی‌توان عدد 1 رشته‌ای را با یک فیلد از نوع int در سمت سرور مقایسه کرد.
- در اینجا serverPaging، serverFiltering و serverSorting نیز به true تنظیم شده‌اند. اگر این مقدار دهی‌ها صورت نگیرد، این اعمال در سمت کلاینت انجام خواهند شد.

پس از تعریف DataSource، تنها کافی است آن‌را به خاصیت dataSource یک kendoGrid نسبت دهیم.
- autoBind: true سبب می‌شود تا اطلاعات DataSource بدون نیاز به فراخوانی متد read آن به صورت خودکار دریافت شوند.
- با تنظیم scrollable: false، اعلام می‌کنیم که قرار است تمام رکوردها در معرض دید قرارگیرند و اسکرول پیدا نکنند.
- pageable: true صفحه بندی را فعال می‌کند. این مورد نیاز به تنظیم pageSize: 10 در قسمت DataSource نیز دارد.
- با sortable: true مرتب سازی ستون‌ها با کلیک بر روی سرستون‌ها فعال می‌گردد.
- filterable: true به معنای فعال شدن جستجوی خودکار بر روی فیلدها است. کتابخانه‌ی Kendo.DynamicLinq حاصل آن‌را در سمت سرور مدیریت می‌کند.
- reorderable: true سبب می‌شود تا کاربر بتواند محل قرارگیری ستون‌ها را تغییر دهد.
- ذکر columnMenu: true اختیاری است. اگر ذکر شود، امکان مخفی سازی انتخابی ستون‌ها نیز مسیر خواهد شد.
- در آخر ستون‌های گرید مشخص شده‌اند. با تعیین "{format: "{0:c سبب نمایش فیلدهای قیمت با سه رقم جدا کننده خواهیم شد. مقدار ریال آن از فایل فرهنگ جاری تنظیم شده دریافت می‌گردد. با استفاده از template تعریف شده نیز سبب نمایش فیلد bool به صورت یک checkbox خواهیم شد.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید:
KendoUI03.zip
مطالب
کوئری نویسی در EF Core - قسمت پنجم - اعمال تجمعی - بخش دوم
کوئری‌های تجمعی این قسمت، کمی پیچیده‌تر هستند و برای حل آن‌ها باید از window functions استفاده کرد و چون این مفهوم توسط EF-Core پشتیبانی نمی‌شود (منظور توسط LINQ to Entities آن است و نه SQL نویسی مستقیم)، در بعضی از موارد مجبور خواهیم شد اطلاعات مورد نیاز گزارش را از بانک اطلاعاتی دریافت کرده و سپس در سمت کلاینت توسط LINQ to Objects شکل دهی کنیم.


مثال 12: محاسبه کنید در سال 2012 و به ازای هر ماه مجزای آن، چه تعداد slots رزرو شده‌اند؛ قسمت دوم.

این مثال را در قسمت قبل (مثال 6 آن) نیز بررسی کردیم. در اینجا می‌خواهیم در گزارش نهایی تولید شده، پس از اتمام ردیف‌های یک ماه به ازای یک امکان خاص، جمع کل آن نیز درج شود و همچنین در پایان تمام ردیف‌ها، جمع کل نهایی ذکر شود؛ چیزی شبیه به تصویر زیر که در آن 910، جمع کل slots ماه 8 است و 9191، جمع کل سال.


روش پیشنهادی حل این مساله استفاده از مفهومی به نام «GROUP BY ROLLUP» است:
SELECT   facid,
         DATEPART(month, [StartTime]) AS month,
         sum(slots) AS slots
FROM     bookings
WHERE    starttime >= '2012-01-01'
         AND starttime < '2013-01-01'
GROUP BY ROLLUP(facid, DATEPART(month, [StartTime]))
ORDER BY facid, month;
یک چنین گروه بندی توسط LINQ to Entities پشتیبانی نمی‌شود. اما خلاصه‌ی این گزارش به این صورت است:
ابتدا جمع slots را گروه بندی شده بر اساس هر ماه سال محاسبه می‌کنیم. این قسمت توسط LINQ to Entities قابل انجام است؛ همان مثال 6 قسمت قبل است.
سپس این اطلاعات که اکنون در سمت کلاینت (یعنی برنامه‌ی ما) در حافظه موجود هستند، نیاز دارند به ازای هر گروه، یک جمع کل (sub total) و به ازای کل سال نیز یک جمع کل (grand total یا total) پیدا کنند.

ROLLUP(facid, month) اطلاعات تجمعی سلسه مراتبی پارامترهای ارسالی به آن را تولید می‌کند. یعنی (facid, month), (facid) و (). پیاده سازی LINQ to Objects این تابع را در اینجا می‌توانید مشاهده کنید: Utils\GroupingExtensions.cs

بنابراین راه حل این مساله به صورت زیر خواهد بود:
var date1 = new DateTime(2012, 01, 01);
var date2 = new DateTime(2013, 01, 01);

var facilities = context.Bookings
                                    .Where(booking => booking.StartTime >= date1
                                                        && booking.StartTime < date2)
                                    .GroupBy(booking => new { booking.FacId, booking.StartTime.Month })
                                    .Select(group => new
                                    {
                                        group.Key.FacId,
                                        group.Key.Month,
                                        TotalSlots = group.Sum(booking => booking.Slots)
                                    })
                                    .OrderBy(result => result.FacId)
                                        .ThenBy(result => result.Month)
                                    .ToList()
                            //This is new
                            .GroupByWithRollup(
                                item => item.FacId,
                                item => item.Month,
                                (primaryGrouping, secondaryGrouping) => new
                                {
                                    FacId = primaryGrouping.Key,
                                    Month = secondaryGrouping.Key,
                                    TotalSlots = secondaryGrouping.Sum(item => item.TotalSlots)
                                },
                                item => new
                                {
                                    FacId = item.Key,
                                    Month = -1,
                                    TotalSlots = item.SubTotal(subItem => subItem.TotalSlots)
                                },
                                items => new
                                {
                                    FacId = -1,
                                    Month = -1,
                                    TotalSlots = items.GrandTotal(subItem => subItem.TotalSlots)
                                });
تا جائیکه متد ToList فراخوانی شده، همان مثال 6 قسمت قبل است. پس از آن چون این لیست را درون حافظه داریم، اکنون متد الحاقی جدید GroupByWithRollup را به آن اعمال می‌کنیم تا اطلاعات گروه بندی اصلی، اطلاعات subTotal (همان ردیف اضافه‌ی تولید شده‌ی حاصل جمع هر گروه) و total (یا همان ردیف جمع کل گزارش) را تولید کند.
در اینجا سلول‌هایی که اطلاعاتی ندارند، با منهای یک مشخص شده‌اند؛ در گزارش اصلی با null مقدار دهی شده بودند.


مثال 13: به ازای نام هر کدام از امکانات موجود، جمع کل تعداد ساعات رزرو شده‌ی آن‌ها را محاسبه کنید.

هر slot تنها نیم ساعت است و گزارش نهایی باید به همراه ستون‌های facid, name, Total Hours باشد؛ مرتب شده بر اساس facid.
var items = context.Bookings
                                    .GroupBy(booking => new { booking.FacId, booking.Facility.Name })
                                    .Select(group => new
                                    {
                                        group.Key.FacId,
                                        group.Key.Name,
                                        TotalHours = group.Sum(booking => booking.Slots) / 2M
                                    })
                                    .OrderBy(result => result.FacId)
                                    .ToList();
در اینجا روش گروه بندی بر اساس FacId که از جدول Bookings تامین می‌شود و Facility.Name را که از جدول دیگری به نامFacilities  تامین می‌شود، ملاحظه می‌کنید که به صورت خودکار جوین لازم آن در کوئری نهایی تولید خواهد شد:



مثال 14: گزارشی را از اولین رزرو کاربران پس از September 1st 2012، تهیه کنید.

این گزارش باید به همراه ستون‌های surname, firstname, memid, starttime باشد؛ مرتب شده بر اساس memid.
var date1 = new DateTime(2012, 09, 01);
var items = context.Bookings
                                    .Where(booking => booking.StartTime >= date1)
                                    .GroupBy(booking => new
                                    {
                                        booking.Member.Surname,
                                        booking.Member.FirstName,
                                        booking.Member.MemId
                                    })
                                    .Select(group => new
                                    {
                                        group.Key.Surname,
                                        group.Key.FirstName,
                                        group.Key.MemId,
                                        StartTime = group.Min(booking => booking.StartTime)
                                    })
                                    .OrderBy(result => result.MemId)
                                    .ToList();
هدف از این مثال محاسبه‌ی حداقل StartTime‌ها به ازای اطلاعات گروه بندی شده‌ی بر اساس هر کاربر است که روش آن‌را با استفاده از متد group.Min مشاهده می‌کنید.



مثال 15: گزارشی را از کاربران تهیه کنید که هر ردیف آن، به همراه تعداد کل کاربران باشد.

این گزارش باید به همراه ستون‌های count, firstname, surname باشد؛ مرتب شده بر اساس joindate.
var members = context.Members
                        .OrderBy(member => member.JoinDate)
                        .Select(member => new
                        {
                            Count = context.Members.Count(),
                            member.FirstName,
                            member.Surname
                        })
                        .ToList();
EF-Core این گزارش به همراه یک sub-query را تبدیل به دو کوئری می‌کند؛ ابتدا مقدار ثابت تعداد اعضاء را محاسبه می‌کند و سپس این تعداد ثابت را در کوئری دوم بکار می‌گیرد:
SELECT COUNT(*)
FROM   [Members] AS [m];

SELECT   [m].[FirstName],
         [m].[Surname],
         @__Count_0 AS [Count]
FROM     [Members] AS [m]
ORDER BY [m].[JoinDate];


مثال 16:  گزارشی را از کاربران تهیه کنید که به همراه ستون شماره ردیف آن‌ها نیز باشد.

باید بخاطر داشت که ID کاربران پشت سرهم نیست و همچنین این گزارش باید به همراه ستون‌های row_number, firstname, surname باشد؛ مرتب شده بر اساس joindate.

هدف اصلی از این مثال، کار با مفهوم window function‌ها و تابع row_number است:
SELECT   row_number() OVER (ORDER BY joindate) AS row_number,
         firstname,
         surname
FROM     members
ORDER BY joindate;
اما چون چنین قابلیتی با LINQ to Entities قابل پیاده سازی نیست، در اینجا نیز ابتدا ردیف‌های گزارش را تولید می‌کنیم و سپس شماره ردیف را در سمت کلاینت (در سمت برنامه و توسط LINQ to Objects)، اضافه خواهیم کرد:
var members = context.Members
                        .OrderBy(member => member.JoinDate)
                        .Select(member => new
                        {
                            member.FirstName,
                            member.Surname
                        })
                        .ToList()
                        /*
                            SELECT [m].[FirstName], [m].[Surname]
                                FROM [Members] AS [m]
                                ORDER BY [m].[JoinDate]
                        */
                        // Now using LINQ to Objects
                        .Select((member, index) => new
                        {
                            RowNumber = index + 1,
                            member.FirstName,
                            member.Surname
                        })
                        .ToList();
تا قسمت ToList، یک کوئری LINQ to Entities استاندارد مشاهده می‌شود. پس از آن چون این اطلاعات درون حافظه هستند، می‌توان با استفاده از LINQ to Objects و قابلیت index ذاتی موجود در متد Select، شماره ردیف‌ها را که همان index + 1 هستند، تولید کرد.


مثال 17: کدامیک از امکانات موجود، بیشترین slots رزرو شده را دارد؟ قسمت دوم.

این مورد همان مثال 11 قسمت قبل است که پاسخ آن‌را یافتیم (و از تکرار مجدد آن صرفنظر می‌کنیم) و هدف اصلی آن رسیدن به کوئری window function دار زیر است که تنها از طریق اجرای یک raw sql در EF-Core قابل اجرا است:
SELECT facid,
       total
FROM   (SELECT   facid,
                 sum(slots) AS total,
                 rank() OVER (ORDER BY sum(slots) DESC) AS rank
        FROM     bookings
        GROUP BY facid) AS ranked
WHERE  rank = 1;


مثال 18: به کاربران بر اساس تعداد ساعات رزرو آن‌ها، امتیاز دهی (رتبه بندی) کنید.

این گزارش باید به همراه ستون‌های firstname, surname, hours, rank باشد؛ مرتب شده بر اساس rank, surname.

هدف اصلی از این مثال، رسیدن به کوئری rank دار زیر است:
SELECT   mems.firstname,
         mems.surname,
         ((sum(bks.slots) + 10) / 20) * 10 AS hours,
         rank() OVER (ORDER BY ((sum(bks.slots) + 10) / 20) * 10 DESC) AS rank
FROM     bookings AS bks
         INNER JOIN
         members AS mems
         ON bks.memid = mems.memid
GROUP BY mems.firstname,
         mems.surname
ORDER BY rank, mems.surname, mems.firstname;
هرچند نمی‌توان از window functions به همراه LINQ to Entities استفاده کرد، اما می‌توان نتیجه‌ای را که خواسته (تولید rank بر اساس تعداد ساعات استفاده شده) به صورت زیر نیز تولید کرد که شامل استفاده‌ی از LINQ to Objects هم نمی‌شود؛ یعنی برای تولید Rank، الزاما نیازی به Window Functions نیست:
var itemsQuery = context.Bookings
                                    .GroupBy(booking => new
                                    {
                                        booking.Member.FirstName,
                                        booking.Member.Surname
                                    })
                                    .Select(group => new
                                    {
                                        group.Key.FirstName,
                                        group.Key.Surname,
                                        Hours = (group.Sum(booking => booking.Slots) + 10) / 20 * 10
                                    })
                                    .OrderByDescending(result => result.Hours)
                                        .ThenBy(result => result.Surname)
                                        .ThenBy(result => result.FirstName);
                var rankedItems = itemsQuery.Select(thisItem => new
                {
                    thisItem.FirstName,
                    thisItem.Surname,
                    thisItem.Hours,
                    Rank = itemsQuery.Count(mainItem => mainItem.Hours > thisItem.Hours) + 1
                })
                .ToList();
در ابتدا یک کوئری متداول گروه بندی شده‌ی بر اساس کاربران را مشاهده می‌کنید که به ازای هر کاربر، جمع تعداد ساعات رزور شده‌ی او محاسبه شده‌است. البته itemsQuery یک IQueryable مرتب سازی شده‌است؛ یعنی چون هنوز ToList بر روی آن فراخوانی نشده، بر روی بانک اطلاعاتی اجرا نشده‌است و فقط یک LINQ Expression است. سپس این LINQ Expression را به صورت زنجیروار در یک کوئری دیگر استفاده کرده‌ایم که در آن sub-query دارای itemsQuery.Count، مقدار rank را تشکیل داده‌است. این ساب کوئری به این معنا است: چه تعداد ساعت حاصل از کوئری گروه بندی و مرتب شده، از مقدار ساعت ردیف جاری بیشتر است + 1 که رتبه‌ی هر ردیف را نسبت به ردیف‌های دیگر محاسبه می‌کند.

با این خروجی SQL نهایی:



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

این گزارش باید به همراه ستون‌های name, rank باشد؛ مرتب شده بر اساس rank.

روش محاسبه‌ی این گزارش با مثال قبلی یکی است (البته اینبار رتبه بندی بر اساس TotalRevenue است) و فقط در انتهای آن یک Where(result => result.Rank <= 3) را بیشتر دارد:
var facilitiesQuery =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .OrderBy(result => result.TotalRevenue);

                var rankedFacilities = facilitiesQuery.Select(thisItem => new
                {
                    thisItem.Name,
                    thisItem.TotalRevenue,
                    Rank = facilitiesQuery.Count(mainItem => mainItem.TotalRevenue > thisItem.TotalRevenue) + 1
                })
                .Where(result => result.Rank <= 3)
                .OrderBy(result => result.Rank)
                .ToList();
ابتدا به نحو متداولی گروه بندی بر اساس نام صورت گرفته و محاسبه‌ی میزان فروش هر گروه انجام شده‌است. سپس در کوئری زنجیروار دوم، ستون Rank، به نتیجه‌ی حاصل اضافه شده‌است و اگر این Rank کمتر از 3 باشد، پاسخ مساله‌است.



مثال 20: امکانات موجود را بر اساس میزان فروشی که دارند به گروه‌هایی با تعداد مساوی high, average, low تقسیم بندی کنید.

این گزارش باید به همراه ستون‌های name, revenue باشد؛ مرتب شده بر اساس revenue, name.

هدف اصلی از این گزارش کار با تابع ntile است که اطلاعات را بر اساس پارامتر ارسالی به آن تاجای ممکن به گروه‌های مساوی تقسیم می‌کند:
SELECT   name,
         CASE WHEN class = 1 THEN 'high' WHEN class = 2 THEN 'average' ELSE 'low' END AS revenue
FROM     (SELECT   facs.name AS name,
                   ntile(3) OVER (ORDER BY sum(CASE WHEN memid = 0 THEN slots * facs.guestcost ELSE slots * membercost END) DESC) AS class
          FROM     bookings AS bks
                   INNER JOIN
                   facilities AS facs
                   ON bks.facid = facs.facid
          GROUP BY facs.name) AS subq
ORDER BY class, name;
Ntile نیز در LINQ to Entities معادلی ندارد. بنابراین ابتدا رزروهای انجام شده را بر اساس نوع امکانات رزرو شده، گروه بندی کرده و میزان فروش هر گروه را پیدا می‌کنیم:
var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.Name)
                                .Select(group => new
                                {
                                    Name = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                })
                                .OrderByDescending(result => result.TotalRevenue)
                                .ToList();
که یک چنین SQL ای را تولید می‌کند:
SELECT   [f].[Name],
         SUM(CASE WHEN [b].[MemId] = 0 THEN CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[GuestCost] ELSE CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[MemberCost] END) AS [TotalRevenue]
FROM     [Bookings] AS [b]
         INNER JOIN
         [Facilities] AS [f]
         ON [b].[FacId] = [f].[FacId]
GROUP BY [f].[Name]
ORDER BY SUM(CASE WHEN [b].[MemId] = 0 THEN CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[GuestCost] ELSE CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[MemberCost] END) DESC;
سپس با استفاده از LINQ to Objects، تابع ntile را شبیه سازی می‌کنیم:
var n = 3;
var tiledFacilities = facilities.Select((item, index) =>
                                        new
                                        {
                                            Item = item,
                                            Index = (index / n) + 1
                                        })
                                        .GroupBy(x => x.Index)
                                        .Select(g =>
                                            g.Select(z =>
                                                new
                                                {
                                                    z.Item.Name,
                                                    z.Item.TotalRevenue,
                                                    Tile = g.Key,
                                                    GroupName = g.Key == 1 ? "High" : (g.Key == 2 ? "Average" : "Low")
                                                })
                                                .OrderBy(x => x.GroupName)
                                                    .ThenBy(x => x.Name)
                                        )
                                        .ToList();

var flatTiledFacilities = tiledFacilities.SelectMany(group => group)
                                        .Select(tile => new { tile.Name, Revenue = tile.GroupName })
                                        .ToList();
هدف از این گزارش این است که در نتیجه‌ی مرتب سازی شده‌ی بر اساس TotalRevenue، به سه تای اول، برچسب High را بدهیم، به سه تای دوم برچسب average و به مابقی برچسب low. به همین جهت ردیف‌های حاصل را بر اساس ستون جدیدی به نام Index که بیانگر شماره ردیف گروه‌های سه تایی است، گروه بندی می‌کنیم و به هر گروه برچسبی را انتساب می‌دهیم. حاصل آن، گروه‌های تو در تویی است که با SelectMany، نسبت به مسطح سازی آن‌ها اقدام شده‌است.


مثال 21: چندماه طول می‌کشد تا هر کدام از امکانات موجود بر اساس فروشی که دارند، هزینه‌ی مالکیت ابتدایی خود را کسب کنند.

این گزارش باید به همراه ستون‌های name, months باشد؛ مرتب شده بر اساس name.
var facilities =
                            context.Bookings.Select(booking =>
                                new
                                {
                                    booking.Facility.Name,
                                    booking.Facility.InitialOutlay,
                                    booking.Facility.MonthlyMaintenance,
                                    Revenue = booking.MemId == 0 ?
                                            booking.Slots * booking.Facility.GuestCost
                                            : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => new
                                {
                                    b.Name,
                                    b.InitialOutlay,
                                    b.MonthlyMaintenance
                                })
                                .Select(group => new
                                {
                                    group.Key.Name,
                                    RepayTime =
                                        group.Key.InitialOutlay /
                                                ((group.Sum(b => b.Revenue) / 3) - group.Key.MonthlyMaintenance)
                                })
                                .OrderBy(result => result.Name)
                                .ToList();
ابتدا رزروهای انجام شده را بر اساس نوع امکانات رزرو شده گروه بندی کرده و میزان فروش هر گروه را پیدا می‌کنیم. سپس بر روی این حاصل، محاسبات خاص RepayTime را انجام داده و نتیجه را بازگشت می‌دهیم:



مثال 22: گزارش میانگین متحرک فروش کل هر کدام از روزهای August 2012 را برای یک بازه‌ی 15 روزه‌ی قبل، محاسبه کنید.

این گزارش باید به همراه ستون‌های date, revenue باشد؛ مرتب شده بر اساس date. در این گزارش روزهای ماه 8 میلادی ردیف شده و به ازای هر ردیف، میانگین فروش 15 روز قبل از آن تاریخ، نمایش داده می‌شود. به همین جهت به آن میانگین متحرک نیز می‌گویند.

هدف اصلی از این گزارش، استفاده از توابع avg(revdata.rev) over است. اما چون نمی‌توان از آن‌ها در LINQ to Entities استفاده کرد، از روش دیگری که شامل جوین یک جدول با خودش است، استفاده می‌کنیم:
var startDate = new DateTime(2012, 08, 1);
var endDate = new DateTime(2012, 08, 31);
var period = 14;

var dailyRevenueQuery =
                        context.Bookings
                                .Select(booking =>
                                new
                                {
                                    StartDate = booking.StartTime.Date, // How to group by date (or TruncateTime) in EF-Core
                                    Revenue = booking.MemId == 0 ?
                                                           booking.Slots * booking.Facility.GuestCost
                                                           : booking.Slots * booking.Facility.MemberCost
                                })
                                .GroupBy(b => b.StartDate)
                                .Select(group =>
                                new
                                {
                                    Date = group.Key,
                                    TotalRevenue = group.Sum(b => b.Revenue)
                                });
ابتدا میزان کل فروش‌ها را بر حسب تاریخ هر روز ماه 8 میلادی، محاسبه می‌کنیم. برای این گروه بندی خاص نیاز خواهیم داشت تا از زمان یک تاریخ صرفنظر کنیم (چون StartTime به همراه تاریخ و ساعت است). برای اینکار فقط کافی است بجای  booking.StartTime از booking.StartTime.Date استفاده شود تا نتیجه‌ی حاصل به CONVERT(date, [b0].[StartTime]) ترجمه شده و قسمت زمان تاریخ از کوئری نهایی حذف شود.
اکنون که میزان کل فروش روزها را داریم، می‌خواهیم میانگین فروش 15 روز قبل شروع شده‌ی از از ابتدای ماه 8، تا انتهای آن‌را محاسبه کنیم. برای اینکار نیاز است کوئری فوق را یکبار دیگر با خودش جوین کنیم تا از یک سر آن تاریخ هر روز و از طرف دیگر، میانگین 15 روز قبل، تولید شود:
var movingAvgs =
                        dailyRevenueQuery
                                .Select(dr1 =>
                                new
                                {
                                    dr1.Date,
                                    MovingAvg = dailyRevenueQuery
                                        .Where(dr2 => dr2.Date <= dr1.Date && dr2.Date >= dr1.Date.AddDays(-period))
                                        .Average(dr2 => dr2.TotalRevenue)
                                })
                                .Where(result => result.Date >= startDate && result.Date <= endDate)
                                .OrderBy(result => result.Date)
                                .ToList();



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
نظرات مطالب
تبدیل HTML به PDF با استفاده از کتابخانه‌ی iTextSharp
تعداد تگ‌هایی که iTextSharp ساپورت می‌کنه کم هست. لیست این‌ها رو می‌تونید در کلاس HtmlTags فضای نام iTextSharp.text.html مشاهده کنید.

یک توصیه کلی:
اگر به دنبال یک راه حل حرفه‌ای برای کارهای پیچیده‌تر HTML to PDF هستید، باید سراغ این نوع کتابخانه‌ها بروید:
wkhtmltopdf ,Convert html to pdf using webkit (qtwebkit)
برای مثال این مورد از WebKit یا همان موتور گوگل کروم استفاده می‌کند. بنابراین HTML parser آن مانند iTextSharp محدود نیست و فوق ‌العاده حرفه‌ای است.
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 2 - ثبت اولین سرویس
یک پروژه‌ی ASP.NET Core را با قرار دادن نسخه‌ی NET Core. بر روی 3.1 و با استفاده از قالب Model View Controller ایجاد کنید. در اینجا نام پروژه را AspNetCoreDependencyInjection گذاشته‌ام. حالا در  پوشه‌ی Models، فایلی را با نام HomeViewModel.cs با محتویاتی به صورت زیر اضافه کنید:
public class HomeViewModel
{
     public string Id { get; set; }
     public string Message { get; set; }
     public DateTime DateTime { get; set; }
}

اکنون به پوشه‌ی Views بروید و فایل Index.cshtml را به این صورت تغییر دهید:

@model AspNetCoreDependencyInjection.Models.HomeViewModel
@{
ViewData["Title"] = "Home";
}

<div>
<div>
<div>
<p>
<b>Id : </b><span>@Model.Id</span> <br />
<b>Date And Time : </b><span> @Model.DateTime </span> <br/>
<b>Message : </b><span>@Model.Message</span>
</p>
</div>
</div>
</div>
و فایل MessageServiceA.cs را به پروژه اضافه کنید:
using AspNetCoreDependencyInjection.Services;

namespace AspNetCoreDependencyInjection.ServicesImplentaions
{
    public class MessageServiceAA 
    {
        public string Message()
        {
            return "A message from MessageServiceAA";
        }
    }
}
و همچنین فایل GuidHelper.cs را نیز اضافه می‌کنیم:
namespace AspNetCoreDependencyInjection.Helpers
{
    public class GuidProvider
    {
        private readonly Guid _serviceGuid;

        public GuidProvider()
        {
            _serviceGuid = Guid.NewGuid();
        }

        public Guid GetNewGuid() => Guid.NewGuid();

        public string GetGuidAsFormatedString(string prefix = "") => getFormatedGuid(_serviceGuid, prefix);

        private string getFormatedGuid(Guid guid, string prefix = "")
        {
            var guidString = guid.GetHashCode().ToString("x");
            if (string.IsNullOrEmpty(prefix) == false)
                guidString = new StringBuilder($"{prefix}-").Append(guidString).ToString();
            return guidString;
        }
    }
}

حالا درون کنترل HomeController، این تغییرات را انجام می‌دهیم:

private readonly ILogger<HomeController> _logger;
private readonly MessageServiceAA _messageService;
private readonly GuidProvider _ guidProvider;

public HomeController(ILogger<HomeController> logger)
{
            _logger = logger;
            _messageService = new MessageServiceAA();
            _guidProvider = new GuidProvider();
}

public IActionResult Index()
{
            var model = new HomeViewModel()
            {
                Id = _ guidProvider.GetGuidAsFormatedString(),
                Message = _messageService.Message(),
                DateTime = DateTime.Now,
            };
            return View(model);
}

همانطور که می‌بینید، در کد بالا، کنترلر HomeController، به دو شیء از کلاس‌ها و یا سرویس‌های GuidProvider و MessageServiceAA به صورت مستقیم وابسته شده‌است و با هر تغییری در هر کدام از این سرویسها، باید دوباره کامپایل شود. علاوه بر این اگر بخواهیم پیاده سازی‌های مختلفی را برای هر کدام از این موارد، ارائه دهیم، به مشکل بر می‌خوریم. خب بیاید تغییراتی را در کد بالا بدهیم تا مشکلات ذکر شده را حل کنیم.

برای این منظور پوشه‌ای را به نام Services می‌سازیم و اینترفیسی را به نام IMessageBrokerA ایجاد می‌کنیم و سپس کاری می‌کنیم که MessageServiceAA از این اینترفیس ارث بری کند:

namespace AspNetCoreDependencyInjection.Services
{
    public interface IMessageServiceA
    {
        string Message();
    }
}

و حالا می‌خواهیم با استفاده از تزریق وابستگی، وابستگی کنترلر HomeController را از کلاس MessageBrokerAA لغو کرده و آن را به اینترفیس IMessageBrokerA (انتزاع) وابسته کنیم. در اینجا ما از تکنیک تزریق درون سازنده یا Constructor Injection استفاده می‌کنیم.


تزریق درون سازنده

در این تکنیک، ما لیستی از وابستگی‌های مورد نیاز را به عنوان پارامترهای ورودی سازنده‌ی کلاس، تعریف می‌کنیم:
private readonly ILogger<HomeController> _logger;
private readonly IMessageServiceA _messageService;
private readonly GuidProvider _guidHelper;
public HomeController(ILogger<HomeController> logger , IMessageServiceA messageService)
{
        _logger = logger;
        _messageService = messageService;
        _messageService = new MessageServiceAA();
        _guidHelper = new GuidProvider();
}
و حالا اگر برنامه را اجرا کنیم، با خطایی روبه رو می‌شویم که در آن می‌گوید امکان واکشی (Resolve) سرویس‌های مورد نظر وجود ندارد. این خطا به دلیل ثبت نشدن اینترفیس IMessageServiceA و پیاده سازی آن، درون Microsoft Dependency Injection Container است   DI Container‌ها معمولا باید در زمان شروع برنامه، پیکربندی و مقدار دهی شودند، تا در ادامه‌ی چرخه‌ی حیات برنامه، بتوانند سرویس‌ها و اشیاء مورد نیاز را به کلاس‌هایی که نیاز دارند، واکشی و تزریق کنند. اولین مرحله از کار با DI Container‌ها، ثبت کردن سرویس‌ها درون آنهاست.
در ASP.NET Core از IServiceCollection برای ثبت کردن سرویس‌های برنامه‌ی خودمان استفاده می‌کنیم. تمامی سرویس‌هایی را که انتظار داریم توسط DI Container به کلاس‌هایی تزریق شوند، باید درون IServiceCollection ثبت گردند. تمام سرویس‌هایی که به وسیله‌ی IServiceCollection ثبت شده‌اند، پس از ساخته شدن، توسط اینترفیس IServiceProvider قابل واکشی هستند.

بنابراین دو اینترفیس حیاتی برای کار کرد صحیح Microsoft Dependency Injection Container درون ASP.NET Core وجود دارند:
  • IServiceCollection : برای ثبت سرویس‌ها
  • IServiceProvider : برای واکشی سرویس‌ها

در ASP.NET Core معمول‌ترین مکان برای ثبت کردن سرویس‌ها درون Container، به صورت پیش فرض درون کلاس Startup و درون متد ConfigureServices انجام می‌گیرد.
به صورت پیش فرض کلاس Startup دو متد دارد:
  • ConfigureServices : برای پیکربندی و ثبت سرویس‌های درونی DI Container استفاده می‌شود.
  • Configure : برای تنظیمات pipeline میان افزارها ( Middlewares ) بکار می‌رود.

در اینجا پیاده سازی پیش فرض کلاس Startup را می‌بینیم که البته کدهای درون متد Configure را برای درگیر نکردن ذهن شما، مخفی کرده‌ایم: 
public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //  کدها جهت خوانایی بیشتر مخفی شده اند
        }
    }

همانطورکه می‌بینید، متد ConfigureService پارامتر IServiceCollection را می‌گیرد که به وسیله‌ی WebHost در زمان اجرای برنامه، مقدار دهی می‌شود.

تعداد زیادی Extension method برای IServiceCollection وجود دارند که برای پشتیبانی از ثبت کردن سرویس‌های مختلف در سناریوهای گوناگون به کار می‌روند. در اینجا ما از نسخه‌ی 3.1 چارچوب ASP.NET Core استفاده می‌کنیم. برای همین هم برای ثبت سرویس‌های پیش فرض فریمورک MVC از متد توسعه‌ی services.AddControllersWithViews()    استفاده می‌کنیم.  متد توسعه‌ی AddControllersWithViews() سرویس‌هایی را که معمولا در فریم ورک MVC استفاده می‌شوند، درون IServiceCollection ثبت می‌کند. در نسخه‌های قبلی چارچوب ASP.NET Core،  مانند نسخه‌های 2.1 و 2.2 برای این کار از متد توسعه‌ی AddMvc() استفاده می‌شد.

در Microsoft Dependency Injection Container ، معمولا  ترتیب ثبت سرویس‌ها مهم نیست.

خب، اولین سرویس اختصاصی برنامه‌ی خودمان را با چرخه‌ی حیات Transient و زیر سرویس پیشین، به شکل زیر ثبت می‌کنیم :

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            services.AddTransient<IMessageServiceA, MessageServiceAA>();
        }
همانطور که می‌بینید، در اینجا ما از متد AddTransient() استفاده کرده‌ایم. متد AddTransient() درون فضای نام Microsoft.Extensions.DependencyInjection قرار دارد. این متد Overload ‌های گوناگونی دارد و ما از نوعی از آن استفاده کرده‌ایم که دو نوع generic را می‌پذیرد و تعریف آن به صورت زیر است: 
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
در اینجا TService ، اینترفیس سرویس ماست. این نوع، همان نوعی است که کلاس‌های ما می‌توانند به آن وابسته باشند. پارامتر دوم، از نوع TImplemention است که پیاده سازی مورد نظر برای TService را ثبت می‌کند. TImplmention   نوعی است که Container در زمان واکشی و تزریق TService از آن نمونه سازی کرده و به کلاس مورد نظر تزریق می‌کند.

در اینجا وقتی ما برای IMessageServiceA ، پیاده سازی MessageServiceA را ثبت می‌کنیم، از این به بعد DI Container، هر زمانیکه در لیست پارامترهای سازنده‌ی یک کلاس، IMessageServiceA را مشاهده کند، بررسی می‌کند که چه کلاسی به عنوانی پیاده سازی این اینترفیس ثبت شده‌است، سپس از آن نمونه سازی می‌کند و درون سازنده‌ی مورد نظر تزریق می‌کند. خب، حالا برنامه را دوباره اجرا کنید؛ می‌بینید که برنامه اجرا می‌شود.

 
در ادامه ابتدا در مورد روش‌های مختلف ثبت سرویس‌ها و بعد روش‌های واکشی سرویس‌ها را بررسی می‌کنیم.
نظرات مطالب
مروری سریع بر اصول مقدماتی MVVM
سلام
تشکر از مطالب بسیار مفیدتون
من چندین ساله دارم برنامه نویسی میکنم و جدیدن با تکنولوژی WPF آشناشدم. متاسفانه هر مرجعی که برای یادگیری این اصول (WPF , MVVM,...) که بکار بردم در اول کار مطالب خیلی پیش پا افتاده رو بیان میکنند و بدون گفتن پیش زمینه های لازم وارد مباحث بسیار سنگین میشن. که باعث میشه آموزنده از مطلب زده بشه.(یه جورایی هم احساس حقارت در مورد سواد کم خودش بهش دست بده) در هر صورت من علاقه بسیار زیادی به برنامه نویسی داشتم و دارم وخیلی دوست دارم با تکنولوژی های جدید بیشتر کار کنم و سبک کاریم رو بروز کنم. از شما که در این زمینه تجربه کافی دارین میخوام لطف کنین یک منبع و مرجع برای یادگیری این مباحث (مباحث جدید که یادگیریش واسه برنامه نویسی الان از نون شب واجب تره) چه فارسی چه انگلیسی معرفی کنین. لازم به ذکره که مطالب آموزشی که خود شما میذارید تقریبا از سواد الان من فراتره و خیلی از قسمت هاشو درک نمیکنم(که قطعا به خاطر سواد کم من در این زمینه است).
پیشاپیش از لطفتون ممنونم.
نظرات مطالب
خواندنی‌های 23 فروردین
با سلام،
دوست عزیز. من اکیداً پیشنهاد می کنم سعی در فارسی کردن دانش رایانه نکنید. این خیانت است به آینده علمی کشور. محافظت از زبان پارسی وظیفه تک تک ما ایرانیان است. اما با راه و روش درستش. این علم و اکثر متون فنی آن به زبان انگلیسی بوده و کاربرد روزانه لغات فنی به زبان اصلی، باعث بالا بردن امکان مطالعه مطالب روز توسط تمام سطوح کارشناسان کشور و دانشجویان در این رشته و در سطوح مختلف آن می شود. چه علاقه ای به ریختن آب در چرخ مترجمان دارید ؟؟ آنها کار با ارزشی می کنند. اما ترجمه متون فنی کار عجیبی است وقتی می توان این مسئله را به خصوص در مورد زبانهای فنی حل کرد. واقعاً به نظر شما "دایر کردن فرو رفتنی" کاربردی زیبا و معنا دار است ؟ خواهش می کنم بجز حفظ زبان پارسی به آینده و حال علم کامپیوتر در ایران نیز بیاندیشید. سایت بسیار جالب و به روزی دارید. خسته نباشید و متشکرم.
مطالب
Angular Material 6x - قسمت چهارم - نمایش پویای اطلاعات تماس‌ها
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت می‌خواهیم این لیست را با اطلاعات دریافت شده‌ی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آن‌ها را نیز در قسمت main-content نمایش دهیم.



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

سرویس Web API ارائه شده‌ی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشت‌های آن‌ها به سمت کلاینت باز می‌گرداند و ساختار موجودیت‌های آن‌ها به صورت زیر است:

موجودیت کاربر که یک رابطه‌ی one-to-many را با UserNotes دارد:
using System;
using System.Collections.Generic;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class User
    {
        public User()
        {
            UserNotes = new HashSet<UserNote>();
        }

        public int Id { set; get; }
        public DateTimeOffset BirthDate { set; get; }
        public string Name { set; get; }
        public string Avatar { set; get; }
        public string Bio { set; get; }

        public ICollection<UserNote> UserNotes { set; get; }
    }
}
و موجودیت یادداشت‌های کاربر که سر دیگر رابطه را تشکیل می‌دهد:
using System;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class UserNote
    {
        public int Id { set; get; }
        public DateTimeOffset Date { set; get; }
        public string Title { set; get; }

        public User User { set; get; }
        public int UserId { set; get; }
    }
}
در نهایت اطلاعات ذخیره شده‌ی در بانک اطلاعاتی توسط سرویس کاربران:
    public interface IUsersService
    {
        Task<List<User>> GetAllUsersIncludeNotesAsync();
        Task<User> GetUserIncludeNotesAsync(int id);
    }
در اختیار کنترلر Web API برنامه، برای ارائه‌ی به سمت کلاینت، قرار می‌گیرد:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _usersService.GetAllUsersIncludeNotesAsync());
        }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> Get(int id)
        {
            return Ok(await _usersService.GetUserIncludeNotesAsync(id));
        }
    }
}
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژه‌ی پیوستی انتهای بحث می‌توانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
 https://localhost:5001/api/users
یک چنین خروجی قابل مشاهده خواهد بود:


و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت می‌دهد.


تنظیم محل تولید خروجی Angular CLI

ساختار پوشه بندی پروژه‌ی جاری به صورت زیر است:


همانطور که ملاحظه می‌کنید، کلاینت Angular در یک پوشه‌است و برنامه‌ی سمت سرور ASP.NET Core در پوشه‌ای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشه‌ی wwwroot پروژه‌ی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش می‌کنیم:
"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
تنظیم این outputPath به wwwroot پروژه‌ی وب سبب خواهد شد تا با صدور فرمان زیر:
 ng build --no-delete-output-path --watch
برنامه‌ی Angular در حالت watch (گوش فرا دادان به تغییرات فایل‌ها) کامپایل شده و سپس به صورت خودکار در پوشه‌ی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامه‌ی ASP.NET Core توسط دستور زیر:
 dotnet watch run
 این برنامه‌ی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامه‌ی Angular می‌شود.
بنابراین دو صفحه‌ی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشه‌ی MaterialAngularClient) و در دومی dotnet watch run را در پوشه‌ی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا می‌شوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژه‌ها ایجاد کردید، صرفا همان قسمت کامپایل می‌شود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.


تعریف معادل‌های کلاس‌های موجودیت‌های سمت سرور، در برنامه‌ی Angular

در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادل‌های کلاس‌های موجودیت‌های سمت سرور خود را به صورت اینترفیس‌هایی تایپ‌اسکریپتی تعریف کنیم:
ng g i contact-manager/models/user
ng g i contact-manager/models/user-note
این دستورات دو اینترفیس خالی کاربر و یادداشت‌های او را در پوشه‌ی جدید models ایجاد می‌کنند. سپس آن‌ها را به صورت زیر و بر اساس تعاریف سمت سرور آن‌ها، تکمیل می‌کنیم:
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote {
  id: number;
  title: string;
  date: Date;
  userId: number;
}
محتویات فایل contact-manager\models\user.ts :
import { UserNote } from "./user-note";

export interface User {
  id: number;
  birthDate: Date;
  name: string;
  avatar: string;
  bio: string;

  userNotes: UserNote[];
}


ایجاد سرویس Angular دریافت اطلاعات از سرور

ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد می‌کنیم:
 ng g s contact-manager/services/user --no-spec
که سبب ایجاد فایل user.service.ts در پوشه‌ی جدید services خواهد شد:
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor() { }
}
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجم‌تر کردن خروجی نهایی برنامه‌است؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشده‌است. به این ترتیب دیگر نیازی نیست تا آن‌را به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شده‌ی UserService را در ذیل مشاهده می‌کنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  getAllUsersIncludeNotes(): Observable<User[]> {
    return this.http
      .get<User[]>("/api/users").pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  getUserIncludeNotes(id: number): Observable<User> {
    return this.http
      .get<User>(`/api/users/${id}`).pipe(
        map(response => response || {} as User),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آن‌ها بحث شده‌است.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشت‌های آن‌ها از سرور واکشی می‌کند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشت‌های او از سرور دریافت می‌کند.


بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material

بسته‌ی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوه‌ی «افزودن آیکن‌های متریال به برنامه» را بررسی کردیم که در آنجا آیکن‌های مرتبط، از فایل‌های قلم، دریافت و نمایش داده می‌شوند. این کامپوننت، علاوه بر قلم آیکن‌ها، از فایل‌های svg حاوی آیکن‌ها نیز پشتیبانی می‌کند که یک نمونه از این فایل‌ها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شده‌است (چون برنامه‌ی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
    <svg viewBox="0 0 128 128" height="100%" width="100%" 
             pointer-events="none" display="block" id="user1" >
هر svg تعریف شده‌ی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوه‌ی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماس‌ها را با صورت زیر تکمیل می‌کنیم:
import { Component } from "@angular/core";
import { MatIconRegistry } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";

@Component()
export class ContactManagerAppComponent {

  constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
    iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg"));
  }
  
}
MatIconRegistry جزئی از بسته‌ی angular/material است که در ابتدای کار import شده‌است. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکن‌های مختلف را دریافت می‌کند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آن‌را ملاحظه می‌کنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامه‌های Angular» بیشتر بحث شده‌است.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon(
      "unicorn",
      this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg")
    );
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر می‌تواند نمایش دهد:
 <mat-icon svgIcon="unicorn"></mat-icon>

یک نکته: پوشه‌ی node_modules\material-design-icons به همراه تعداد قابل ملاحظه‌ای فایل svg نیز هست.


نمایش لیست کاربران در sidenav

در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق می‌کنیم:
import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit {

  users: User[] = [];

  constructor(private userService: UserService) {  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => this.users = data);
  }
}
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخ‌داد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت می‌گیرد.

اکنون می‌خواهیم از این اطلاعات جهت نمایش پویای آن‌ها در sidenav استفاده کنیم. در قسمت قبل، جای آن‌ها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
اگر به مستندات mat-list مراجعه کنیم، در میانه‌ی صفحه، navigation lists نیز ذکر شده‌است که می‌تواند لیستی پویا را به همراه لینک به آیتم‌های آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض می‌کنیم:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine href="#">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:


که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آن‌ها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.


اتصال کاربران به صفحه‌ی نمایش جزئیات آن‌ها

در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شده‌است. در ادامه می‌خواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine [routerLink]="['/contactmanager', user.id]">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحه‌ی جزئیات آن شخص، متصل کرده‌ایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: ":id", component: MainContentComponent },
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشاره‌گر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمی‌دهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کرده‌ایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-main-content",
  templateUrl: "./main-content.component.html",
  styleUrls: ["./main-content.component.css"]
})
export class MainContentComponent implements OnInit {

  user: User;
  constructor(private route: ActivatedRoute, private userService: UserService) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.user = null;

      const id = params["id"];
      if (!id) {
        return;
      }

      this.userService.getUserIncludeNotes(id)
        .subscribe(data => this.user = data);
    });
  }
}
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت می‌شود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون می‌توان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user">
  <mat-spinner></mat-spinner>
</div>
<div *ngIf="user">
  <mat-card>
    <mat-card-header>
      <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon>
      <mat-card-title>
        <h2>{{ user.name }}</h2>
      </mat-card-title>
      <mat-card-subtitle>
        Birthday {{ user.birthDate | date:'d LLLL' }}
      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <mat-tab-group>
        <mat-tab label="Bio">
          <p>
            {{user.bio}}
          </p>
        </mat-tab>
        <!-- <mat-tab label="Notes"></mat-tab> -->
      </mat-tab-group>
    </mat-card-content>
  </mat-card>
</div>
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کرده‌ایم. اگر user نال باشد، این spinner نمایش داده می‌شود و برعکس.


همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کرده‌ایم (اگر دقت کنید، هر کامپوننت آن سه برگه‌ی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره می‌توان با کامپوننت‌های این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا می‌کنیم که مناسب کار ما باشد. سپس سورس آن‌را از همانجا کپی و در برنامه قرار می‌دهیم و در آخر آن‌را بر اساس اطلاعات خود سفارشی سازی می‌کنیم.



نمایش جزئیات اولین کاربر در حین بارگذاری اولیه‌ی برنامه

تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماس‌ها ظاهر می‌شود و همانطور باقی می‌ماند، با اینکه هنوز موردی انتخاب نشده‌است. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظه‌ی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش می‌دهیم:
import { Router } from "@angular/router";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  
  constructor(private userService: UserService, private router: Router) {
  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => {
        this.users = data;
        if (data && data.length > 0 && !this.router.navigated) {
          this.router.navigate(["/contactmanager", data[0].id]);
        }
      });
  }
}
در اینجا ابتدا سرویس Router به سازنده‌ی کلاس تزریق شده‌است و سپس زمانیکه کار دریافت اطلاعات تماس‌ها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال می‌کنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن می‌شود.

البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id'];
if (!id) id = 1;
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر می‌گیریم.


بستن خودکار sidenav در حالت نمایش موبایل

اگر اندازه‌ی صفحه‌ی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتم‌های آن‌را انتخاب کنیم، هرچند آن‌ها نمایش داده می‌شوند، اما زیر این sidenav مخفی باقی خواهند ماند:


بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینه‌های آن، این قسمت بسته شده و ناحیه‌ی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شده‌است:
<mat-sidenav #sidenav
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
import { MatSidenav } from "@angular/material";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  @ViewChild(MatSidenav) sidenav: MatSidenav;
اکنون که به این ViewChild دسترسی داریم، می‌توانیم در حالت نمایشی موبایل، متد close آن‌را فراخوانی کنیم:
  ngOnInit() {
    this.router.events.subscribe(() => {
      if (this.isScreenSmall) {
        this.sidenav.close();
      }
    });
  }
در اینجا با مشترک this.router.events شدن، متوجه‌ی کلیک کاربر و نمایش صفحه‌ی جزئیات آن می‌شویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینه‌ای کلیک کرده بود و همچنین اندازه‌ی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آن‌را مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
مطالب
انواع روش‌های خلاقانه تولید رشته‌های تصادفی
حتما برای شما هم پیش آمده‌است که در پروژه‌ای نیاز داشتید تا رشته‌ای تصادفی را تولید کنید. کد تصادفی میتواند کاربردهایی چون تولید رمز، تولید شناسه، تولید url ، تولید کد فعال سازی و مواردی از این قبیل را داشته باشد.
احتمالا برای ساخت کد یا رشته تصادفی، اولین چیزی که به ذهن شما می‌رسد، استفاده از کلاس random می‌باشد. اما روش‌های خلاقانه و جالب زیادی وجود دارند که برای این کار استفاده می‌شوند. در اینجا می‌خواهیم تعدادی از آنها را با هم بررسی کنیم.


روش‌های تولید اعداد یا رشته تصادفی: 

1- معمول‌ترین روش تولید یک کد شش رقمی با استفاده از کلاس random 

[TestMethod]
public void TestRandomClass()
{
    var code = new Random().Next(100000, 999999);
    Assert.IsTrue(code.ToString().Length == 6);
}

2- تولید با استفاده کلاس Random و Enumerable
[TestMethod]
public void TestRandomWithEnumerable()
{
    var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    var random = new Random();
    var result = new string(
        Enumerable.Repeat(chars, 6)
        .Select(s => s[random.Next(s.Length)])
        .ToArray());
    Assert.IsTrue(result.Length == 6);
}
البته بدیهی هست که در قسمت chars می‌توانید هر نوع کاراکتری را قرار دهید و کد نهایی بر آن مبنا تولید می‌شود. مثلا می‌توانید فقط اعداد را مشخص کنید و در این حالت رشته‌ی خروجی فقط شامل رقم خواهد بود. اگر خواستید رشته‌ی طولانی‌تری را تولید کنید، کافی ست طول مورد نیاز را با عدد 6 در کد بالا جایگزین کنید.
مثلا برای تولید رمز عبور از لیست زیر می‌تونید استفاده کنید:
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuwxyz~!@#$%^&*";

3- تولید کد با استفاده از guid
Guid.NewGuid().ToString().Replace("-", string.Empty).Substring(0, 6);
یا
Guid.NewGuid().ToString("n").Substring(0, 6);
کد ("ToString("n کاراکترهای غیرعددی را از رشته‌ی مورد نظر حذف میکند.

4 - تولید با استفاده از کلاس RNGCryptoServiceProvider 
بعضی‌ها روش‌های ویژه را می‌پسندند. البته استفاده از این کلاس مزایا و معایب خودش را دارد. از نظر سرعت نسبت به کلاس random پایین‌تر هست، چون محاسبات بیشتری دارد.
public static string GetUniqueKey(int maxSize)
        {
            char[] chars = new char[62];
            chars =
            "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
            byte[] data = new byte[1];
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                crypto.GetNonZeroBytes(data);
                data = new byte[maxSize];
                crypto.GetNonZeroBytes(data);
            }
            StringBuilder result = new StringBuilder(maxSize);
            foreach (byte b in data)
            {
                result.Append(chars[b % (chars.Length)]);
            }
            return result.ToString();
        }
    }

5 - استفاده از متد Path.GetRandomFileName 
کاربرد اصلی این متد در واقع تولید نام فایلی تصادفی است؛ ولی از آن برای تولید رشته هم استفاده میکنند. 
متد Path.GetRandomFileName  در پشت صحنه از همان کلاس RNGCryptoServiceProvider  برای تولید نام فایل استفاده میکند.
public string Get8CharacterRandomString()
{
    string path = Path.GetRandomFileName();
    path = path.Replace(".", ""); // Remove period.
    return path.Substring(0, 6);  // Return 6 character string
}

6- تولید کد با استفاده از کلاس random و linq
var chars = "abcdefghijklmnopqrstuvwxyz123456789".ToArray();
string pw = Enumerable.Range(0, passwordLength)
                      .Aggregate(
                          new StringBuilder(),
                          (sb, n) => sb.Append((chars[random.Next(chars.Length)])),
                          sb => sb.ToString());

نتیجه
مطمئنا روش‌های زیادی برای تولید رشته تصادفی وجود دارند و البته همه شباهت‌هایی نیز دارند و در لایه‌های یایین‌تر، دارای اصولی مشترک هستند.موارد بالا فقط روش‌های متفاوت تولید کد نهایی را نشان می‌دهند که شما بسته به نیاز خود میتوانید از آنها استفاده کنید.
شما چه روش‌هایی را برای این کار می‌شناسید؟