نظرات مطالب
استفاده از Awesomium.NET در برنامه‌های وب
فایل‌های csproj و sln آن‌را حذف کنید. بعد دو پروژه وب فرم و MVC خالی درست کنید و فایل‌های موجود را به آن‌ها اضافه کنید. ماژول آن هم یک class library ساده است.
نظرات مطالب
Globalization در ASP.NET MVC
MVC 4 فقط یک سری افزونه بیشتر از MVC3 داره. مثلا razor آن بهبود پیدا کرده، فشرده سازی فایل‌های CSS به اون اضافه شده یا Web API رو به صورت یکپارچه داره. از لحاظ کار با فایل‌های منبع فرقی نکرده.
نظرات مطالب
معرفی Blazor Hybrid
ببینید NET MAUI App که از اساس با Xaml کار می‌کنه و البته Native UI محسوب می‌شه

Blazor Hybrid یا همون NET MAUI Blazor App راهکار Blazor هست که HTML/CSS برای UI استفاده می‌شه، ولی C# .NET اش دسترسی کامل به سیستم عامل داره (بر خلاف Blazor Web Assembly که محدود به Sandbox مرورگر هست)

پس با Blazor Web Assembly & Server می‌تونیم PWA / SPA / Web Site داشته باشیم و طبیعتا محدود به Sandbox مرورگر هست
با Blazor Hybird هم Android / iOS / Windows app داریم که دسترسی به امکانات سیستم عامل هست که UI با HTML / CSS زده می‌شه

یا Blazor Native شما دیگه UI تون HTML/CSS نیست و برای داشتن TextBox به جای input type=text، از Entry استفاده می‌کنید برای مثال که پشت صحنه مپ می‌شه به کنترل‌های Native در Android / iOS / Windows

توصیه من این هست که Blazor رو به صورت Multi Mode تنظیم کنید، به صورتی که UI رو با HTML / CSS بزنید، و هم خروجی Android بگیرید و هم iOS و Web و ویندوز
برای درک بهتر این مسئله می‌تونید وبینارم رو در رابطه با 
what's new in dotnet 6  ببینید
نظرات مطالب
OpenCVSharp #6
- وجود Thread Sleep با مقداری که در مطلب فوق عنوان شده، ضروری هست. از این جهت که اساسا رابط کاربری معمولی ویندوز، قابلیت پردازش تعداد عظیمی از پیام‌های رسیده را ندارد و باید در این بین به آن فرصت داد. بحث DirectShow مجموعه‌ی Direct-X متفاوت است و طراحی اختصاصی آن برای یک چنین کارهایی است. اما در اینجا نمی‌توانید UI معمولی را با سیلی از داده‌ها و پیام‌های به روز رسانی، مدفون کنید.
- استفاده از متد capture.QueryFrame().ToBitmap اشتباه هست. از این جهت که خروجی capture.QueryFrame می‌تواند نال باشد. بنابراین این تبدیل را باید در داخل حلقه انجام دهید و نه در زمانیکه قصد دارید تصویری را دریافت کنید. شرط موجود در حلقه (مانند مثال اصلی مطلب)، بررسی نال نبودن این فریم دریافتی است. بنابراین اگر نال باشد، حلقه پایان خواهد یافت.
- همانطور که در متن عنوان شد، متد workerProgressChanged در ترد اصلی یا همان ترد UI اجرا می‌شود. بنابراین فراخوانی pictureBoxIpl1.Invoke غیر ضروری است و سربار بی‌جهتی را به سیستم تحمیل می‌کند.
به صورت خلاصه در حین استفاده‌ی از BackgroundWorker:
- متد رخداد گردان DoWork بر روی ThreadPool اجرا می‌شود (ترد آن با ترد UI یکی نیست)
- متد‌های رخدادگردان گزارش پیشرفت کار و اتمام کار، بر روی ترد UI اجرا می‌شوند. بنابراین امکان دسترسی به عناصر UI در این متدها، بدون مشکلی وجود دارد.
مطالب
کار با Kendo UI DataSource
Kendo UI DataSource جهت تامین داده‌های سمت کلاینت ویجت‌های مختلف KendoUI طراحی شده‌است و به عنوان یک اینترفیس استاندارد قابل استفاده توسط تمام کنترل‌های داده‌ای Kendo UI کاربرد دارد. Kendo UI DataSource امکان کار با منابع داده محلی، مانند اشیاء و آرایه‌های جاوا اسکریپتی و همچنین منابع تامین شده از راه دور، مانند JSON، JSONP و XML را دارد. به علاوه توسط آن می‌توان اعمال ثبت، ویرایش و حذف اطلاعات، به همراه صفحه بندی، گروه بندی و مرتب سازی داده‌ها را کنترل کرد.


استفاده از منابع داده محلی
در ادامه مثالی را از نحوه‌ی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var cars = [
                { "Year": 2000, "Make": "Hyundai", "Model": "Elantra" },
                { "Year": 2001, "Make": "Hyundai", "Model": "Sonata" },
                { "Year": 2002, "Make": "Toyota", "Model": "Corolla" },
                { "Year": 2003, "Make": "Toyota", "Model": "Yaris" },
                { "Year": 2004, "Make": "Honda", "Model": "CRV" },
                { "Year": 2005, "Make": "Honda", "Model": "Accord" },
                { "Year": 2000, "Make": "Honda", "Model": "Accord" },
                { "Year": 2002, "Make": "Kia", "Model": "Sedona" },
                { "Year": 2004, "Make": "Fiat", "Model": "One" },
                { "Year": 2005, "Make": "BMW", "Model": "M3" },
                { "Year": 2008, "Make": "BMW", "Model": "X5" }
            ];

            var carsDataSource = new kendo.data.DataSource({
                data: cars
            });

            carsDataSource.read();
            alert(carsDataSource.total());
        });
    </script>
در اینجا cars آرایه‌ای از اشیاء جاوا اسکریپتی بیانگر ساختار یک خودرو است. سپس برای معرفی آن به Kendo UI، کار با مقدار دهی خاصیت data مربوط به new kendo.data.DataSource شروع می‌شود.
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آن‌را جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت می‌دهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.


استفاده از منابع داده راه دور

در برنامه‌های کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوه‌ی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var twitterDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "http://search.twitter.com/search.json",
                        dataType: "jsonp",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET',
                        data: { q: "#kendoui" }
                    },
                    schema: { data: "results" }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
در قسمت transport، جزئیات تبادل اطلاعات با سرور راه دور مشخص می‌شود؛ برای مثال url ارائه دهنده‌ی سرویس، dataType بیانگر نوع داده مورد انتظار و data کار مقدار دهی پارامتر مورد انتظار توسط سرویس توئیتر را انجام می‌دهد. در اینجا چون صرفا عملیات خواندن اطلاعات صورت می‌گیرد، خاصیت read مقدار دهی شده‌است.
در قسمت schema مشخص می‌کنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.


کار با منابع داده OData

علاوه بر فرمت‌های یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
    <script type="text/javascript">
            var moviesDataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: "http://demos.kendoui.com/service/Northwind.svc/Orders"
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
همانطور که ملاحظه می‌کنید، تنظیمات ابتدایی آن اندکی با حالت remote data پیشین متفاوت است. در اینجا ابتدا نوع داده‌ی بازگشتی مشخص می‌شود و در قسمت transport، خاصیت read آن، آدرس سرویس را دریافت می‌کند.


یک مثال: دریافت اطلاعات از ASP.NET Web API

یک پروژه‌ی جدید ASP.NET را آغاز کنید. تفاوتی نمی‌کند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
using System.Collections.Generic;
using System.Web.Http;

namespace KendoUI02
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }

    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            var products = new List<Product>();
            for (var i = 1; i <= 100; i++)
            {
                products.Add(new Product { Id = i, Name = "Product " + i });
            }
            return products;
        }
    }
}
در این مثال، هدف صرفا ارائه یک خروجی ساده JSON از طرف سرور است.
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
using System;
using System.Web.Http;
using System.Web.Routing;

namespace KendoUI02
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
        }
    }
}

در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta 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.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>
</head>
<body>

    <div id="report-grid"></div>
    <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'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                columns: [
                    { field: "Id", title: "#" },
                    { field: "Name", title: "Product" }
                ]
            });
        });
    </script>
</body>
</html>
- ابتدا فایل‌های اسکریپت و CSS مورد نیاز Kendo UI اضافه شده‌اند.
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره می‌کند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش داده‌ایم.
- نحوه‌ی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شده‌اند. این دو خاصیت بر روی نحوه‌ی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص می‌کند و sort نحوه‌ی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار می‌دهد.
- در ادامه، ابتدایی‌ترین حالت کار با kendoGrid را ملاحظه می‌کنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستون‌ها را فعال می‌کنند.
- در آخر هم دو ستون گرید، بر اساس نام‌های خواص کلاس Product تعریف شده‌اند.


سورس کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI02.zip
 
مطالب
ساخت رابط کاربری برای الکترون
قدرت الکترون برگرفته از فناوری وب است و هر آنچه که در آنجا امکان پذیر باشد، در اینجا نیز امکان پذیر است و خصوصیت برنامه‌های دسکتاپ را نیز داراست. الکترون به دلیل بارگذاری فایل‌های html، به شما اجازه می‌دهد تا از ابزارهایی چون بوت استرپ و فریمورک‌ها و کیت‌های مشابهی چون جی‌کوئری و انگیولار، امبر Ember و ... در آن استفاده کنید. ولی با این حال، الکترون نوپا سعی دارد کیت‌های اختصاصی خودش را هم داشته باشد، که در این مقاله به آن‌ها اشاره می‌کنیم.
یکی از این کیت‌ها، فوتون نام دارد. فوتون شامل یک سری css ,sass، فونت و قالب‌های html است که به شما اجازه می‌دهد تا یک برنامه با ظاهری شبیه به برنامه‌های مک را داشته باشید. کامپوننت‌های فوتون شامل tab ها، لیست‌ها، منوی کناری، دکمه‌های معمولی یا جعبه ابزاری و کنترل‌های فرم‌ها می‌شود. با این حال اگر هم دوست ندارید که از این کامپوننت‌ها استفاده کنید، می‌توانید از layout ‌های آن استفاده کند که پنجره‌ی شما را تقسیم بندی می‌کنند.
برای استفاده از فوتون لازم است آن را دانلود کرده و فایل‌های آن را در پروژه‌ی خود کپی کنید. دایرکتوری dist آن شامل یک مثال می‌شود که می‌توانید آن را به داخل دایرکتوری پروژه خود کپی کنید؛ یا خود این دایرکتوری را به عنوان دایرکتوری پروژه تعیین کنید. بعد از آن، فایل‌های موجود در دایرکتوری template را به داخل دایرکتوری والد، یعنی dist انتقال دهید و داخل فایل html، مسیر فایل css را تصحیح نمایید. فقط می‌ماند که الکترون را بر روی این محل نصب کنید، یا اینکه الکترون نصب شده‌ی به صورت عمومی (Global) را در اختیار آن قرار دهید.
بعد از آن ممکن است با خطا مواجه شوید و وقتی فایل اصلی را که در اینجا نام آن app.js است، باز کنید، خطوط زیر را می‌بینید:
var app=require('app');
var BrowserWindow=require('browser-window');
این نوع استفاده از ماژول‌های داخلی، متعلق به نسخه‌های اولیه است و در نسخه‌های اخیر پشتیبانی نمی‌شود. پس بهتر است این خطوط را به صورت‌هایی که قبلا گفته‌ایم تغییر دهید.
سپس برنامه را اجرا کنید تا رابط جدید کاربری را ببینید.
فقط یک مشکلی هست و آن هم این است که باید فریم یا پنجره‌ای را که خود الکترون تولید می‌کند، حذف کنیم برای حذف آن می‌توانید از خصوصیت frame در شیء Browser Window استفاده کنید:
if(process.platform=='darwin')
{
  mainWindow= new BrowserWindow({
    width: 1000,
    height: 500,
    'min-width': 1000,
    'min-height': 500,
    'accept-first-mouse': true,
    'title-bar-style': 'hidden',
    titleBarStyle:'hidden'
  });
}
else {
  new BrowserWindow({
   width: 1000,
   height: 500,
   'min-width': 1000,
   'min-height': 500,
   frame:false
 });
}
در نسخه‌های 10 به بعد مک، از آنجاکه این خصوصیت، نه تنها فریم کرومیوم را حذف میکند، بلکه قابلیت‌هایی چون تغییر اندازه و ... را از آن نیز می‌گیرد، برای همین خصوصیت titleBarStyle را که به دو شکل هم می‌تواند نوشته شود، مورد استفاده قرار می‌دهیم.
حالا اگر برنامه را مجددا اجرا کنید، می‌بینید که قاب‌های دور آن حذف شده‌اند، ولی با چند ثانیه کار کردن متوجه این ایراد می‌شوید که پنجره قابل درگ کردن و جابجایی نمی‌باشد. برای حل آن باید از css کمک بگیریم:
-webkit-app-region: drag
دستور بالا را به هر المانی انتساب دهید، آن المان و فرزندانش قابل درگ خواهند بود، ولی اگر المانی را با این خصوصیت تنظیم کردید، ولی قصد دارید که یکی یا چند عدد از المان‌های فرزند این خاصیت را نداشته باشند، این دستور را به آنان انتساب دهید:
-webkit-app-region: no-drag;
از آنجاکه در این رابط کاربری، نوار عنوان تگ مشخصی دارد:
<header class="toolbar toolbar-header" >
با اضافه کردن این دستور css می‌توانید به آن قابلیت درگ را بدهید:
<header class="toolbar toolbar-header" style="-webkit-app-region: drag">
حالا مجددا برنامه را تست کنید تا نتیجه کار را ببینید.
همانطور که می‌بینید با کمترین زحمت، به چنین رابط کاربری رسیدید. تصویر زیر متعلق به برنامه‌ای است که در دو قسمت قبلی (+ + ) ساختیم و حالا با استفاده از این پکیج، ظاهر آن را تغییر داده‌ایم:



Electron UI Kit
دومین رابط کاربری که معرفی میکنیم در واقع یک کیت از یک سری کامپوننت است که بسیار شبیه به برنامه‌های دسکتاپ طراحی شده و شامل لیست‌ها، گریدها، کنترل‌ها و ... است که در دو فایل استایل، برای ویندوز و مک، مجزا شده‌اند.

Maverix

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

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


تنظیمات ابتدایی Kendo UI Editor

در ذیل کدهای سمت کاربر فعال سازی مقدماتی Kendo UI را مشاهده می‌کنید. در قسمت tools آن، لیست امکانات و نوار ابزار مهیای آن درج شده‌اند.
دو مورد insertImage و insertFile آن نیاز به تنظیمات سمت کاربر و سرور بیشتری دارند.
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl">
    <textarea id="editor" rows="10" cols="30" style="height: 440px"></textarea>
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#editor").kendoEditor({
                tools: [
                    "bold", "italic", "underline", "strikethrough", "justifyLeft",
                    "justifyCenter", "justifyRight", "justifyFull", "insertUnorderedList",
                    "insertOrderedList", "indent", "outdent", "createLink", "unlink",
                    "insertImage", "insertFile",
                    "subscript", "superscript", "createTable", "addRowAbove", "addRowBelow",
                    "addColumnLeft", "addColumnRight", "deleteRow", "deleteColumn", "viewHtml",
                    "formatting", "cleanFormatting", "fontName", "fontSize", "foreColor",
                    "backColor", "print"
                ],
                imageBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
                            type: "POST"
                        },
                        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
                        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
                    }
                },
                fileBrowser: {
                    messages: {
                        dropFilesHere: "فایل‌های خود را به اینجا کشیده و رها کنید"
                    },
                    transport: {
                        read: {
                            url: "@Url.Action("GetFilesList", "KendoEditorFiles")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            cache: false
                        },
                        destroy: {
                            url: "@Url.Action("DestroyFile", "KendoEditorFiles")",
                            type: "POST"
                        },
                        create: {
                            url: "@Url.Action("CreateFolder", "KendoEditorFiles")",
                            type: "POST"
                        },
                        uploadUrl: "@Url.Action("UploadFile", "KendoEditorFiles")",
                        fileUrl: "@Url.Action("GetFile", "KendoEditorFiles")?path={0}"
                    }
                }
            });
        });
    </script>
}
در اینجا نحوه‌ی تنظیم مسیرهای مختلف ارسال فایل و تصویر Kendo UI Editor را ملاحظه می‌کنید.
منهای قسمت thumbnailUrl، عملکرد قسمت‌های مختلف افزودن فایل و تصویر این ادیتور یکسان هستند. به همین جهت می‌توان برای مثال کنترلی مانند KendoEditorFilesController را ایجاد و سپس در کنترلر KendoEditorImagesController از آن ارث بری کرد و متد دریافت و نمایش بند انگشتی تصاویر را افزود. به این ترتیب دیگر نیازی به تکرار کدهای مشترک بین این دو قسمت نخواهد بود.


نمایش لیست پوشه‌ها و تصویر در ابتدای باز شدن صفحه‌ی درج تصویر

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


تنظیمات خواندن این فایل‌ها، از قسمت read مربوط به imageBrowser دریافت می‌شود که cache آن نیز به false تنظیم شده‌است تا در این بین مرورگر اطلاعات را کش نکند. این مورد در حین حذف فایل‌ها و پوشه‌ها مهم است. زیرا اگر cache:false تنظیم نشده باشد، حذف یک فایل یا پوشه در سمت کاربر تاثیری نخواهد داشت.
imageBrowser: {
    transport: {
        read: {
            url: "@Url.Action("GetFilesList", "KendoEditorImages")",
            dataType: "json",
            contentType: 'application/json; charset=utf-8',
            type: 'GET',
            cache: false
        }
    }
},
در ادامه نیاز است اکشن متد GetFilesList را به نحو ذیل در سمت سرور تهیه کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpGet]
        public ActionResult GetFilesList(string path)
        {
            path = GetSafeDirPath(path);
            var imagesList = new DirectoryInfo(path)
                                .GetFiles()
                                .Select(fileInfo => new KendoFile
                                {
                                    Name = fileInfo.Name,
                                    Size = fileInfo.Length,
                                    Type = KendoFileType
                                }).ToList();
 
            var foldersList = new DirectoryInfo(path)
                                .GetDirectories()
                                .Select(directoryInfo => new KendoFile
                                {
                                    Name = directoryInfo.Name,
                                    Type = KendoDirType
                                }).ToList();
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(imagesList.Union(foldersList), new JsonSerializerSettings
                {
                    ContractResolver = new CamelCasePropertyNamesContractResolver()
                }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
 
 
        protected string GetSafeDirPath(string path)
        {
            // path = مسیر زیر پوشه‌ی وارد شده
            if (string.IsNullOrWhiteSpace(path))
            {
                return Server.MapPath(FilesFolder);
            }
 
            //تمیز سازی امنیتی
            path = Path.GetDirectoryName(path);
            path = Path.Combine(Server.MapPath(FilesFolder), path);
            return path;
        } 
    }
}
در اینجا کدهای کلاس پایه KendoEditorFilesController را مشاهده می‌کنید. به این جهت فیلد FilesFolder آن protected تعریف شده‌است تا در کلاسی که از آن ارث بری می‌کند نیز قابل دسترسی باشد. سپس لیست فایل‌ها و پوشه‌های path دریافتی با فرمت لیستی از KendoFile تهیه شده و با فرمت JSON بازگشت داده می‌شوند. ساختار KendoFile را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Models
{
    public class KendoFile
    {
        public string Name { set; get; }
        public string Type { set; get; }
        public long Size { set; get; }
    }
}
- در اینجا Type می‌تواند از نوع فایل با مقدار f و یا از نوع پوشه با مقدار d باشد.
- علت استفاده از CamelCasePropertyNamesContractResolver در حین بازگشت JSON نهایی، تبدیل خواص دات نتی، به نام‌های سازگار با JavaScript است. برای مثال به صورت خودکار Name را تبدیل به name می‌کند.
- پارامتر path در ابتدای کار خالی است. اما کاربر می‌تواند در بین پوشه‌های باز شده‌ی توسط مرورگر تصاویر Kendo UI حرکت کند. به همین جهت مقدار آن باید هربار بررسی شده و بر این اساس لیست فایل‌ها و پوشه‌های جاری بازگشت داده شوند.


مدیریت حذف تصاویر و پوشه‌ها

همانطور که در شکل فوق نیز مشخص است، با انتخاب یک پوشه یا فایل، دکمه‌ای با آیکن ضربدر جهت فراهم آوردن امکان حذف، ظاهر می‌شود. این دکمه متصل است به قسمت destroy تنظیمات ادیتور:
imageBrowser: {
    transport: {
        destroy: {
            url: "@Url.Action("DestroyFile", "KendoEditorImages")",
            type: "POST"
        }
    }
},
این تنظیمات سمت کاربر را باید به نحو ذیل در سمت سرور مدیریت کرد:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult DestroyFile(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
 
            var pathToDelete = Path.Combine(path, name);
 
            var attr = System.IO.File.GetAttributes(pathToDelete);
            if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
            {
                Directory.Delete(pathToDelete, recursive: true);
            }
            else
            {
                System.IO.File.Delete(pathToDelete);
            }
 
            return Json(new object[0]);
        } 
    }
}
- استفاده از Path.GetFileName جهت دریافت نام فایل‌ها در اینجا بسیار مهم است. زیرا اگر این تمیز سازی امنیتی صورت نگیرد، ممکن است با کمی تغییر در آن، فایل web.config برنامه، دریافت یا حذف شود.
- پارامتر name دریافتی مساوی است با نام فایل انتخاب شده و path مشخص می‌کند که در کدام پوشه قرار داریم.
- چون در اینجا امکان حذف یک پوشه یا فایل وجود دارد، حتما نیاز است بررسی کنیم، مسیر دریافتی پوشه‌است یا فایل و سپس بر این اساس جهت حذف آن‌ها اقدام صورت گیرد.


مدیریت ایجاد یک پوشه‌ی جدید

تنظیمات قسمت create مرورگر تصاویر، مرتبط است به زمانیکه کاربر با کلیک بر روی دکمه‌ی +، درخواست ایجاد یک پوشه‌ی جدید را کرده‌است:
imageBrowser: {
    transport: {
        create: {
            url: "@Url.Action("CreateFolder", "KendoEditorImages")",
            type: "POST"
        }
    }
},
کدهای اکشن متد متناظر با این عمل را در ذیل مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";
 
        [HttpPost]
        public ActionResult CreateFolder(string name, string path)
        {
            //تمیز سازی امنیتی
            name = Path.GetFileName(name);
            path = GetSafeDirPath(path);
            var dirToCreate = Path.Combine(path, name);
 
            Directory.CreateDirectory(dirToCreate);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Type = KendoDirType
            });
        }
 
        protected ActionResult KendoFile(KendoFile file)
        {
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(file,
                    new JsonSerializerSettings
                    {
                        ContractResolver = new CamelCasePropertyNamesContractResolver()
                    }),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
- در اینجا نیز name مساوی نام پوشه‌ی درخواستی است و path به مسیر تو در توی پوشه‌ی جاری اشاره می‌کند.
- پس از ایجاد پوشه، باید نام آن‌را با فرمت KendoFile به صورت JSON بازگشت داد. همچنین در اینجا Type را نیز باید به d (پوشه) تنظیم کرد.


مدیریت قسمت ارسال فایل و تصویر

زمانیکه کاربر بر روی دکمه‌ی upload file یا بارگذاری تصاویر در اینجا کلیک می‌کند، اطلاعات فایل آپلودی به مسیر uploadUrl ارسال می‌گردد.
imageBrowser: {
    transport: {
        thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")",
        uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")",
        imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}"
    }
},
دو تنظیم دیگر thumbnailUrl و imageUrl، برای نمایش بند انگشتی و نمایش کامل تصویر کاربرد دارند.
در ادامه کدهای مدیریت سمت سرور قسمت آپلود این ادیتور را مشاهده می‌کنید:
namespace KendoUI13.Controllers
{
    public class KendoEditorFilesController : Controller
    {
        //مسیر پوشه فایل‌ها
        protected string FilesFolder = "~/files";
 
        protected string KendoFileType = "f";
        protected string KendoDirType = "d";

 
        [HttpPost]
        public ActionResult UploadFile(HttpPostedFileBase file, string path)
        {
            //تمیز سازی امنیتی
            var name = Path.GetFileName(file.FileName);
            path = GetSafeDirPath(path);
            var pathToSave = Path.Combine(path, name);
 
            file.SaveAs(pathToSave);
 
            return KendoFile(new KendoFile
            {
                Name = name,
                Size = file.ContentLength,
                Type = KendoFileType
            });
        } 
    }
}
- در اینجا path مشخص می‌کند که در کدام پوشه‌ی تو در تو قرار داریم و file نیز حاوی محتوای ارسالی به سرور است.
- پس از ذخیره سازی اطلاعات فایل، نیاز است اطلاعات فایل نهایی را با فرمت KendoFile به صورت JSON بازگشت دهیم.


ارث بری از KendoEditorFilesController جهت تکمیل قسمت مدیریت تصاویر

تا اینجا کدهایی را که ملاحظه کردید، برای هر دو قسمت ارسال تصویر و فایل کاربرد دارند. قسمت ارسال تصاویر برای تکمیل نیاز به متد دریافت تصاویر به صورت بند انگشتی نیز دارد که به صورت ذیل قابل تعریف است و چون از کلاس پایه KendoEditorFilesController ارث بری کرده‌است، این کنترلر به صورت خودکار حاوی اکشن متدهای کلاس پایه نیز خواهد بود.
using System.Web.Mvc;
 
namespace KendoUI13.Controllers
{
    public class KendoEditorImagesController : KendoEditorFilesController
    {
        public KendoEditorImagesController()
        {
            // بازنویسی مسیر پوشه‌ی فایل‌ها
            FilesFolder = "~/images";
        }
 
        [HttpGet]
        [OutputCache(Duration = 3600, VaryByParam = "path")]
        public ActionResult GetThumbnail(string path)
        {
            //todo: create thumb/ resize image
 
            path = GetSafeFileAndDirPath(path);
            return File(path, "image/png");
        }
    }
}

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
شروع کار با Apache Cordova در ویژوال استودیو #4
در قسمت قبل یک مثال ساده را کار کردیم. در این قسمت با jQuery Mobile آشنا شده و در پروژه‌ی خود استفاده خواهیم کرد.

توضیح تکمیلی در مورد ساختار فایل‌های پروژه
همان طور که در قسمتها قبل گفته شد، تگ اسکریپت زیر 
<script src="cordova.js"></script>
از استاندارد‌های Cordova است؛ وجود خارجی ندارد و بخشی از فرآیند ساخت برنامه است.
اگر توجه کنید فایلی با نام platformOverrides.js در فولدر scripts موجود در ریشه، خالی است اما در فولدر merges موجود در ریشه‌ی پروژه مربوط به هر پلتفرم و همنام آن پلتفرم قرار دارد. برای مثال برای android، یک چنین دایرکتوری merges/android/scripts وجود دارد که درون آن فایلی به‌نام  platformOverrides.js دیده می‌شود و اگر دقت کنید، همنام فایل موجود در فولدر scripts موجود در ریشه پروژه است که درون خود فایلی بنام  android2.3-jscompat.js را فراخوانی می‌کند. (برای کمک به سازگاری کتابخانه‌های ثالث)
در زمان build ، تمام فایل‌های موجود در "merges/"platformname ، در فولدر‌های هم نامی در شاخه‌ی ریشه‌ی پروژه کپی شده و جایگزین فایل‌های قبلی خواهند شد.

 مثال برای اندروید 
در زمان ساخت (build) فایل scripts/platformOverrides.js با فایل merges/windows/scripts/platformoverrides.js جایگزین خواهد شد. این امکان برای فلدر‌های css, images و بقیه‌ی آنها نیز امکان پذیر است.
توجه داشته باشید این ادغام در سطح فایل‌ها و نه در سطح محتوای فایل‌ها انجام می‌شود.

نکته 
برای محتوای موجود در فولدر res، قضیه فرق می‌کند. زیرا محتوای این resource‌ها برای اپلیکیشن پکیچ ضروریست؛ پیش از آن که کد‌های ما درون WebView یا host رندر شوند. باید توجه کرد که این فولدر به جهت اینکه منابع اصلی را (با توجه به پلتفرم باید از فایل‌های مشخص آن برای تشخیص ساختار فولدر‌های اپلیکیشن پکیچ استفاده کند) در بر دارد و این منابع باید در زمان ساخت پروژه تشخیص داده شوند.


رویداد‌های بومی
در زیر تعدادی از رخدادهایی که در Cordova گنجانده شده‌اند تا اپلیکیشن ما از رخداد‌های دستگاه با خبر شوند، نشان داده شده است. برای تست آنها به راحتی بعد از اجرای برنامه توسط شبیه ساز Ripple می‌توانید از قسمت Events، رخداد مورد نظر را شبیه سازی کنید:
(function () {
    "use strict";

    document.addEventListener( 'deviceready', onDeviceReady.bind( this ), false );

    function onDeviceReady() {
        // Handle the Cordova pause and resume events
        document.addEventListener( 'pause', onPause.bind( this ), false );
        document.addEventListener('resume', onResume.bind(this), false);
        document.addEventListener('menubutton', onMenuButton.bind(this), false);
        document.addEventListener('backbutton', onBackButton.bind(this), false);
        //document.addEventListener('searchbutton', onResume.bind(this), false);
        //document.addEventListener('endcallbutton', onResume.bind(this), false);
        //document.addEventListener('offline', onResume.bind(this), false);
        //document.addEventListener('online', onResume.bind(this), false);
        //document.addEventListener('startcallbutton', onResume.bind(this), false);
        //document.addEventListener('volumedownbutton', onResume.bind(this), false);
        //document.addEventListener('volumeupbutton', onResume.bind(this), false);
        
        // TODO: Cordova has been loaded. Perform any initialization that requires Cordova here.
    };

    function onPause() {
        // TODO: This application has been suspended. Save application state here.
        alert("paused");
    };

    function onResume() {
        alert("resume");
    };
    function onMenuButton() {
        alert("menu");
    };

    function onBackButton() {
        alert("back button");
    };
   

} )();

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



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

 Supported Devices

  Phones/Tablets 
  Android 1.6+ 
  BlackBerry 5+ 
  iOS 3+ 
  Windows Phone 7 
  WebOS 1.4+ 
  Symbian (Nokia S60) 
  Firefox Mobile Opera Mobile 11+ 
  Opera Mini 5+ 
  Desktop browsers 
  Chrome 11+ 
  Firefox 3.6+ 
  Internet Explorer 7+ 
  Safari   


برای نصب jQuery Mobile کافی است دستورات  زیر را در package manager console ویژوال استودیو استفاده کنید:

PM>install-package jquery

PM>install-package jquery.mobile.rtl

بعد از دانلود فایل‌های مورد نظر خود، فولدری بنام jquery.mobile.rtl در ریشه پروژه ایجاد خواهد شد. به ترتیب فایل های rtl.jquery.mobile-1.4.0.css و rtl.jquery.mobile-1.4.0.js موجود در زیر شاخه‌های فلدر مذکور را به head و آخر body فایل index.html اضافه کنید.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>CordovaApp01</title>

    <!-- CordovaApp01 references -->
    <link href="css/index.css" rel="stylesheet" />
    <link href="jquery.mobile.rtl/css/themes/default/rtl.jquery.mobile-1.4.0.css" rel="stylesheet" />
</head>
<body>
    <div data-role="page" id="page1">
        <div data-role="header">
            <h1>اولین برنامه</h1>
        </div>
        <div data-role="content">
            <p>سلام من محتوای اولین برنامه هستم</p>
        </div>
        <div data-role="footer">
            <h1>من فوتر هستم</h1>
        </div>
    </div>
<!-- Cordova reference, this is added to your app when it's built. -->
<script src="scripts/jquery-2.1.3.min.js"></script>
    <script src="cordova.js"></script>
    <script src="scripts/platformOverrides.js"></script>
    <script src="scripts/index.js"></script>

    <script src="jquery.mobile.rtl/js/rtl.jquery.mobile-1.4.0.js"></script>
</body>
</html>
در تکه کد بالا ما یکی از ویجت‌های jQuery Mobile را استفاده کردیم و با استفاده از ویژگی data-role که برای div اصلی با page مقدار دهی شده است، یک  کانتینر (page container) برای ویجت page جی کوئری موبایل تعریف شده‌است.

نتیجه‌ی نهایی به شکل زیر خواهد بود:

در مقاله‌ی بعد به استفاده از plugin‌ها خواهیم پرداخت.

ادامه دارد...

مطالب
Minimal API's در دات نت 6 - قسمت اول - معرفی
یکی از مهم‌ترین تغییرات دات نت 6، ارائه‌ی Minimal API's به همراه آن است که نسبت به MVC و سایر مشتقات ASP.NET Core، کمتر به همراه پیش‌فرض‌های نظری خاص و بسیار مقید و متعصبانه (opinionated) است؛ که این مورد خود مزیتی است جهت انجام امور متداول، به نحوی دیگر و دلخواه و با آزادی عمل بیشتری در طراحی endpoints مورد نیاز و کل برنامه. خصوصا این سبک جدید، با معماری برش‌های عمودی (vertical slices) ارائه شده‌ی توسط نویسنده‌ی AutoMapper، هماهنگی خاصی دارد و اینطور به نظر می‌رسد که جهت ساده سازی طراحی برنامه‌های ASP.NET Core با معماری CQRS ارائه شده‌است. با وجود Minimal API's می‌توان از دو لایه‌ی متداول برنامه‌ها رها شد: لایه‌ی سرویس‌ها و لایه‌ی مخازن یا Repositories. در معماری برش‌های عمودی، برنامه به ویژگی‌های خاصی (Features) تقسیم شده و هر ویژگی، متکی به خود طراحی می‌شود. زمانیکه از هندلرها برای هر Command و Query معماری CQRS استفاده می‌کنیم، این‌ها مختص به یک ویژگی متکی به خود طراحی می‌شوند و به همراه تمام اطلاعات و اعمال مرتبط هستند و دیگر در این حالت، وجود لایه‌های سرویس و مخزن، بی‌معنا و غیرضروری می‌شوند.

در ادامه قصد داریم تمام این موارد را مانند Minimal API's و معماری برش‌های عمودی به همراه CQRS، در طی یک سری و یک پروژه‌ی عملی ساخت یک Blog به نام MinimalBlog، بررسی کنیم. البته هدف ما در اینجا صرفا ساخت backend ساختار یافته‌ی این برنامه‌است؛ منهای UI آن. هدف اصلی ما از این سری، ارائه‌ی یک معماری، جهت کار با Minimal API's است.


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

جهت مرور سریعتر و ساده‌تر این سری، کدهای کامل آن‌را از اینجا می‌توانید دریافت کنید: MinimalBlog.zip


پروژه‌هایی که برنامه‌ی MinimalBlog را تشکیل می‌دهند

برنامه‌ی MinimalBlog، تنها از سه پروژه‌ی زیر تشکیل می‌شود:
MinimalBlog.Api: این پروژه از نوع minimal API's است که توسط دستور جدید «dotnet new webapi --use-minimal-apis» آغاز خواهد شد و به صورت پیش‌فرض به همراه پشتیبانی از OpenAPI نیز هست. البته اگر از VS2022 استفاده می‌کنید، در حین آغاز یک پروژه‌ی Web API جدید، تیک مربوط به use controllers را در UI بردارید تا از Minimal API's استفاده شود.
MinimalBlog.Dal: که Dal در اینجا مخفف data access layer است و یک class library می‌باشد و با دستور dotnet new classlib آغاز می‌شود.
MinimalBlog.Domain: نیز یک class library است و با دستور dotnet new classlib آغاز می‌شود.

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


بررسی ساختار ابتدایی پروژه‌ی MinimalBlog.Api

در اینجا تنها تک فایل Program.cs، به همراه تنظیمات برنامه قابل مشاهده‌است و فایل Starup.cs از آن حذف شده‌است (اطلاعات بیشتر). این فایل نیز بر مبنای مفهوم top level programs طراحی شده‌است و به همراه تعریف class و یا فضای نامی نیست:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
همانطور که ملاحظه می‌کنید، تمام اتفاقات در همین تک فایل رخ می‌دهند. برای مثال سرویس‌های مورد نیاز برنامه به مجموعه‌ی builder.Services اضافه می‌شوند؛ شبیه به کاری که پیشتر در فایل Startup.cs و متد ConfigureServices آن انجام می‌دادیم.

پس از آن به تعاریف زیر می‌رسیم؛ تعاریف میان افزارهایی که پیشتر در متد Configure کلاس Startup انجام می‌شدند، الان همگی در تک فایل Program.cs قرار دارند:
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
البته هنوز هم می‌توان در صورت نیاز به همان ساختار کلاس Startup پیشین نیز رسید.


در انتهای این فایل نیز تعاریف پیش‌فرض زیر قرار دارند:
var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast =  Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateTime.Now.AddDays(index),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
در اینجا متد متد MapGet یک endpoint را تعریف کرده و سپس اکشنی را به آن انتساب می‌دهد. یعنی اگر آدرس weatherforecast/ درخواست شود، lambda expression تعریف شده، اجرا می‌شود. هدف از ارائه‌ی Minimal API نیز همین است تا بتوان با حداقل کدنویسی، سریعا به نتیجه‌ی مدنظر خود رسید.
در همین حال اگر برنامه‌ی Api را اجرا کنیم، به تصویر زیر خواهیم رسید:


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


غنی سازی Solution و کامپایلر #C با استفاده از فایل‌های editorconfig. و Directory.Build.props

در مورد این دو فایل در مطلب «غنی سازی کامپایلر C# 9.0 با افزونه‌ها » بیشتر بحث شده‌است. هدف از آن‌ها، اعمال یکسری تنظیمات سراسری، به تمام پروژه‌های یک solution به صورت یک‌دست است؛ مانند تنظیمات کامپایلر جهت نمایش اخطارها به صورت خطاها، تعریف usingهای سراسری سی‌شارپ 10 و یا اعمال Roslyn analyzers به تمام پروژه‌ها. این دو فایل را به همراه پروژه‌ی پیوست می‌توانید دریافت کنید و ... باید جزء استاندارد تمام پروژه‌های جدید باشند. چون وجود آن‌ها سبب خواهد شد که به شدت کیفیت کدهای نهایی افزایش یابند و مبتنی بر یکسری best practices شوند.
نظرات اشتراک‌ها
گپ و گفتی با مهندسان طراح دات نت در مورد آینده این فریم ورک

Q: What's the near-term roadmap for .NET tooling?

A: While tentative, here are some short term plans:

  • Sep 2016 – Preview 3 of VS 2015 with early .CSProj support for .NET Core
  • Nov 2016 – .NET Core 1.2, ASP.NET Core 1.2, Entity Framework Core 1.2, SignalR, .NET Standard 2.0, etc.  
به نظرم این زمانبندی ای که داده اصلا درست نیست اولا که قبلا این موارد برای 2017 برنامه ریزی شده بود خصوصا سیگنال آر ثانیا الان NET Standard ورژنش 1.6 چطور تا چندماه این همه میخواد ترقی رتبه کنه ! ثالثا هنوز زمان بندی‌ها تو گیت هاب مثل گذشته است فکر نکنم بشه روی این مطلب حساب کرد
کسی در مورد زمانبندی‌ها اطلاعات جدیدی داره ؟
در کل امیدوارم من اشتباه کنم !