مطالب دوره‌ها
تغییر ترتیب آیتم‌های یک لیست به کمک افزونه jquery.sortable در ASP.NET MVC
در این مطلب قصد داریم ترتیب عناصر نمایش داده شده توسط یک لیست را به کمک افزونه بسیار سبک وزن jquery.sortable تغییر داده و نتایج را در سمت سرور مدیریت کنیم. این افزونه بر اساس امکانات کشیدن و رها ساختن HTML5 کار می‌کند و با مرورگرهای IE8 به بعد سازگار است.

مدل‌های برنامه

using System.Collections.Generic;

namespace jQueryMvcSample05.Models
{
    public class Survey
    {
        public int Id { set; get; }
        public string Title { set; get; }

        public virtual ICollection<SurveyItem> SurveyItems { set; get; }
    }
}

namespace jQueryMvcSample05.Models
{
    public class SurveyItem
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public int Order { set; get; }

        //[ForeignKey("SurveyId")]
        public virtual Survey Survey { set; get; }
        public int SurveyId { set; get; }
    }
}
به کمک این ساختار قصد داریم اطلاعات یک سیستم نظر سنجی را نمایش دهیم.
تعدادی نظر سنجی به همراه گزینه‌های آن‌ها تعریف خواهند شد (یک رابطه one-to-many است). سپس توسط افزونه sortable می‌خواهیم ترتیب قرارگیری گزینه‌های آن‌را مشخص کنیم یا تغییر دهیم.


منبع داده فرضی برنامه


using System.Collections.Generic;
using jQueryMvcSample05.Models;

namespace jQueryMvcSample05.DataSource
{
    /// <summary>
    /// یک منبع داده فرضی جهت دموی ساده‌تر برنامه
    /// </summary>
    public static class SurveysDataSource
    {
        private static IList<Survey> _surveysCache;
        static SurveysDataSource()
        {
            _surveysCache = createSurveys();
        }

        public static IList<Survey> SystemSurveys
        {
            get { return _surveysCache; }
        }

        private static IList<Survey> createSurveys()
        {
            var results = new List<Survey>();
            for (int i = 1; i < 6; i++)
            {
                results.Add(new Survey
                {
                    Id = i,
                    Title = "نظر سنجی " + i,
                    SurveyItems = new List<SurveyItem>
                    {
                       new SurveyItem{ Id = 1, SurveyId = i, Title = "گزینه 1", Order = 1 },
                       new SurveyItem{ Id = 2, SurveyId = i, Title = "گزینه 2", Order = 2 },
                       new SurveyItem{ Id = 3, SurveyId = i, Title = "گزینه 3", Order = 3 },
                       new SurveyItem{ Id = 4, SurveyId = i, Title = "گزینه 4", Order = 4 }
                    }
                });
            }
            return results;
        }
    }
}
در اینجا نیز از یک منبع داده فرضی تشکیل شده در حافظه جهت سهولت دموی برنامه استفاده خواهد شد.


کدهای کنترلر برنامه

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample05.DataSource;
using jQueryMvcSample05.Security;

namespace jQueryMvcSample05.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            var surveysList = SurveysDataSource.SystemSurveys;
            return View(surveysList);
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public ActionResult SortItems(int? surveyId, string[] items)
        {
            if (items == null || items.Length == 0 || surveyId == null)
                return Content("nok");

            updateSurvey(surveyId, items);

            return Content("ok");
        }

        /// <summary>
        /// این متد جهت آشنایی با پروسه به روز رسانی ترتیب گزینه‌ها در اینجا قرار گرفته است
        /// بدیهی است محل قرارگیری آن باید در لایه سرویس برنامه اصلی باشد
        /// </summary>
        private static void updateSurvey(int? surveyId, string[] items)
        {
            var itemIds = new List<int>();
            foreach (var item in items)
            {
                itemIds.Add(int.Parse(item.Replace("item-row-", string.Empty)));
            }

            var survey = SurveysDataSource.SystemSurveys.FirstOrDefault(x => x.Id == surveyId.Value);
            if (survey == null)
                return;

            int order = 0;
            foreach (var itemId in itemIds)
            {
                order++;
                var surveyItem = survey.SurveyItems.FirstOrDefault(x => x.Id == itemId);
                if (surveyItem == null) continue;
                surveyItem.Order = order;
            }

            //todo: call save changes ....
        }
    }
}

و کدهای View متناظر

@model IList<jQueryMvcSample05.Models.Survey>
@{
    ViewBag.Title = "Index";
    var sortUrl = Url.Action(actionName: "SortItems", controllerName: "Home");
}
<h2>
    نظر سنجی‌ها</h2>
@foreach (var survey in Model)
{
    <fieldset>
        <legend>@survey.Title</legend>
        <div id="sortable-@survey.Id">
            @foreach (var surveyItem in survey.SurveyItems.OrderBy(x => x.Order))
            {            
                <div id="item-row-@surveyItem.Id">
                    <span class="handles">::</span>
                    @surveyItem.Title
                </div>
            }
        </div>
    </fieldset>
}
<div>
    لطفا برای تغییر ترتیب آیتم‌های تعریف شده، از امکان کشیدن و رها کردن تعریف شده بر
    روی آیکون‌های :: در کنار هر آیتم استفاده نمائید.
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $('div[id^="sortable"]').sortable({ handle: 'span' }).bind('sortupdate', function (e, ui) {
                var sortableItemId = $(ui.item).parent().attr('id');
                var surveyId = sortableItemId.replace('sortable-', '');
                var items = [];
                $('#' + sortableItemId + ' div').each(function () {
                    items.push($(this).attr('id'));
                });
                //alert(items.join('&'));                
                $.ajax({
                    type: "POST",
                    url: "@sortUrl",
                    data: JSON.stringify({ items: items, surveyId: surveyId }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = "/login";
                        } else if (status === 'error' || !data || data == "nok") {
                            alert('خطایی رخ داده است');
                        }
                        else {
                            alert('انجام شد');
                        }
                    }
                });
            });
        });
    </script>
}
توضیحات

در اینجا نیاز بود تا ابتدا کدهای کنترلر و View ارائه شوند، تا بتوان در مورد ارتباطات بین آن‌ها بهتر بحث کرد.
در ابتدای نمایش صفحه Home، رکوردهای نظرسنجی‌ها از منبع داده دریافت شده و به View ارسال می‌شوند. در View برنامه یک حلقه تشکیل گردیده و این موارد رندر خواهند شد.
هر نظر سنجی با یک div بیرونی که با id مساوی sortable شروع می‌شود، آغاز گردیده و گزینه‌های آن نظر سنجی نیز توسط divهایی با id مساوی item-row شروع خواهند گردید. هر کدام از این idها حاوی id رکوردهای متناظر هستند. از این id‌ها در کدهای برنامه جهت یافتن یک نظر سنجی یا یک ردیف مشخص برای به روز رسانی ترتیب آن‌ها استفاده خواهیم کرد.
ادامه کار، به تنظیمات و اعمال افزونه sortable مرتبط می‌شود. توسط تنظیم ذیل به jQuery اعلام خواهیم کرد، هرجایی یک div با id شروع شده با sortable یافتی، افزونه sortable را به آن متصل کن:
 $('div[id^="sortable"]').sortable
در ادامه در ناحیه و  div ایی که عمل کشیدن و رها شدن رخ داده، id این div را بدست آورده و سپس کلیه row-itemهای آن را در آرایه‌ای به نام items قرار می‌دهیم:
 var sortableItemId = $(ui.item).parent().attr('id');
var surveyId = sortableItemId.replace('sortable-', '');
var items = [];
$('#' + sortableItemId + ' div').each(function () {
  items.push($(this).attr('id'));
});
اکنون که به id یک نظر سنجی و همچنین idهای ردیف‌های مرتب شده حاصل دسترسی داریم، آن‌ها را توسط jQuery Ajax به کنترلر برنامه ارسال می‌کنیم:
 data: JSON.stringify({ items: items, surveyId: surveyId })
امضای اکشن متد SortItems نیز دقیقا بر همین مبنا تنظیم شده است:
 public ActionResult SortItems(int? surveyId, string[] items)

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


دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample05.zip
 
نظرات مطالب
آشنایی با Window Function ها در SQL Server بخش اول
سلام
ممنون از آموزش مفیدتون . سوال من اینه که اگه بخواهیم رکوردهای تکراری حذف بشن باید چیکار کنیم ؟ مثلا من میخام مجموع مبلغ بدهکار برای یک کد کل و در یک سند را در یک ردیف و همین مورد برای مجموع مبلغ بستانکار را نیز در یک رکورد یا ردیف دیگر بدهد . در صورتی که اگر از توابع Sum() Over  استفاده کنیم به ازای هر کد کل درآن سند یک رکورد در خروجی داریم (چه بدهکار و چه بستانکار)
مطالب دوره‌ها
پیاده سازی دکمه «بیشتر» یا «اسکرول نامحدود» به کمک jQuery در ASP.NET MVC
مدتی است در اکثر سایت‌ها و طراحی‌های جدید، به جای استفاده از روش متداول نمایش انتخاب صفحه 1، 2 ... 100، برای صفحه بندی اطلاعات، از روش اسکرول نامحدود یا infinite scroll استفاده می‌کنند. نمونه‌ای از آن‌را هم در سایت جاری با دکمه «بیشتر» در ذیل اکثر صفحات و مطالب سایت مشاهده می‌کنید.


در ادامه قصد داریم نحوه پیاده سازی آن‌را در ASP.NET MVC به کمک امکانات jQuery بررسی کنیم.


مدل برنامه

namespace jQueryMvcSample02.Models
{
    public class BlogPost
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
در این برنامه و مثال، قصد داریم لیستی از مطالب را توسط اسکرول نامحدود، نمایش دهیم. هر آیتم نمایش داده شده، ساختاری همانند کلاس BlogPost دارد.


منبع داده فرضی برنامه

using System.Collections.Generic;
using System.Linq;
using jQueryMvcSample02.Models;

namespace jQueryMvcSample02.DataSource
{
    public static class BlogPostDataSource
    {
        private static IList<BlogPost> _cachedItems;
        /// <summary>
        /// با توجه به استاتیک بودن سازنده کلاس، تهیه کش، پیش از سایر فراخوانی‌ها صورت خواهد گرفت
        /// باید دقت داشت که این فقط یک مثال است و چنین کشی به معنای
        /// تهیه یک لیست برای تمام کاربران سایت است
        /// </summary>
        static BlogPostDataSource()
        {
            _cachedItems = createBlogPostsInMemoryDataSource();
        }

        /// <summary>
        /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است
        /// </summary>        
        private static IList<BlogPost> createBlogPostsInMemoryDataSource()
        {
            var results = new List<BlogPost>();
            for (int i = 1; i < 30; i++)
            {
                results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i });
            }
            return results;
        }

        /// <summary>
        /// پارامترهای شماره صفحه و تعداد رکورد به ازای یک صفحه برای صفحه بندی نیاز هستند
        /// شماره صفحه از یک شروع می‌شود
        /// </summary>
        public static IList<BlogPost> GetLatestBlogPosts(int pageNumber, int recordsPerPage = 4)
        {
            var skipRecords = pageNumber * recordsPerPage;
            return _cachedItems
                        .OrderByDescending(x => x.Id)
                        .Skip(skipRecords)
                        .Take(recordsPerPage)
                        .ToList();
        }
    }
}
برای اینکه برنامه نهایی را به سادگی بتوانید اجرا کنید، به عمد از بانک اطلاعاتی خاصی استفاده نشده و صرفا یک منبع داده فرضی تشکیل شده در حافظه، در اینجا مورد استفاده قرار گرفته است. بدیهی است قسمت cachedItems را به سادگی می‌توانید با یک ORM جایگزین کنید.
تنها نکته مهم آن، نحوه تعریف متد GetLatestBlogPosts می‌باشد که برای صفحه بندی اطلاعات بهینه سازی شده است. در اینجا توسط متدهای Skip و Take، تنها بازه‌ای از اطلاعات که قرار است نمایش داده شوند، دریافت می‌گردد. خوشبختانه این متدها معادل‌های مناسبی را در اکثر بانک‌های اطلاعاتی داشته و استفاده از آن‌ها بر روی یک بانک اطلاعاتی واقعی نیز بدون مشکل کار می‌کند و تنها بازه محدودی از اطلاعات را واکشی خواهد کرد که از لحاظ مصرف حافظه و سرعت کار بسیار مقرون به صرفه و سریع است.


کنترلر برنامه

using System.Linq;
using System.Web.Mvc;
using System.Web.UI;
using jQueryMvcSample02.DataSource;
using jQueryMvcSample02.Security;

namespace jQueryMvcSample02.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult Index()
        {
            //آغاز کار با صفحه صفر است
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber: 0);
            return View(list); //نمایش ابتدایی صفحه
        }

        [HttpPost]
        [AjaxOnly]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult PagedIndex(int? page)
        {
            var pageNumber = page ?? 0;
            var list = BlogPostDataSource.GetLatestBlogPosts(pageNumber);
            if (list == null || !list.Any())
                return Content("no-more-info"); //این شرط ما است برای نمایش عدم یافتن رکوردها

            return PartialView("_ItemsList", list);
        }

        [HttpGet]
        public ActionResult Post(int? id)
        {
            if (id == null)
                return Redirect("/");

            //todo: show the content here
            return Content("Post " + id.Value);
        }
    }
}
کنترلر برنامه را در اینجا ملاحظه می‌کنید. برای کار با اسکرول نامحدود، به ازای هر صفحه، نیاز به دو متد است:
الف) یک متد که بر اساس HttpGet کار می‌کند. این متد در اولین بار نمایش صفحه فراخوانی می‌گردد و اطلاعات صفحه آغازین را نمایش می‌دهد.
ب) متد دومی که بر اساس HttpPost کار کرده و محدود است به درخواستی‌های AjaxOnly همانند متد PagedIndex.
از این متد دوم برای پردازش کلیک‌های کاربر بر روی دکمه «بیشتر» استفاده می‌گردد. بنابراین تنها کاری که افزونه جی‌کوئری تدارک دیده شده ما باید انجام دهد، ارسال شماره صفحه است. سپس با استفاده از این شماره، بازه مشخصی از اطلاعات دریافت و نهایتا یک PartialView رندر شده برای افزوده شدن به صفحه بازگشت داده می‌شود.


دو View برنامه

همانطور که برای بازگشت اطلاعات نیاز به دو اکشن متد است، برای رندر اطلاعات نیز به دو View نیاز داریم:
الف) یک PartialView که صرفا لیستی از اطلاعات را مطابق سلیقه ما رندر می‌کند. از این PartialView در متد PagedIndex استفاده خواهد شد:
@model IList<jQueryMvcSample02.Models.BlogPost>
<ul>
    @foreach (var item in Model)
    {
        <li>
            <h5>
                @Html.ActionLink(linkText: item.Title,
                                 actionName: "Post",
                                 controllerName: "Home",
                                 routeValues: new  { id = item.Id },
                                 htmlAttributes: null)
            </h5>
            @item.Body
        </li>
    }
</ul>
ب) یک View کامل که در بار اول نمایش صفحه، مورد استفاده قرار می‌گیرد:
@model IList<jQueryMvcSample02.Models.BlogPost>
@{
    ViewBag.Title = "Index";
    var loadInfoUrl = Url.Action(actionName: "PagedIndex", controllerName: "Home");
}
<h2>
    اسکرول نامحدود</h2>
@{ Html.RenderPartial("_ItemsList", Model); }
<div id="MoreInfoDiv">
</div>
<div align="center" style="margin-bottom: 9px;">
    <span id="moreInfoButton" style="width: 90%;" class="btn btn-info">بیشتر</span>
</div>
<div id="ProgressDiv" align="center" style="display: none">
    <br />
    <img src="@Url.Content("~/Content/images/loadingAnimation.gif")" alt="loading..."  />
</div>
@section JavaScript
{
    <script type="text/javascript">
        $(document).ready(function () {
            $("#moreInfoButton").InfiniteScroll({
                moreInfoDiv: '#MoreInfoDiv',
                progressDiv: '#ProgressDiv',
                loadInfoUrl: '@loadInfoUrl',
                loginUrl: '/login',
                errorHandler: function () {
                    alert('خطایی رخ داده است');
                },
                completeHandler: function () {
                    // اگر قرار است روی اطلاعات نمایش داده شده پردازش ثانوی صورت گیرد
                },
                noMoreInfoHandler: function () {
                    alert('اطلاعات بیشتری یافت نشد');
                }
            });
        });
    </script>
}
چند نکته در اینجا حائز اهمیت است:
1) مسیر دقیق اکشن متد PagedIndex توسط متد  Url.Action تهیه شده است.
2) در ابتدای نمایش صفحه، متد Html.RenderPartial کار نمایش اولیه اطلاعات را انجام خواهد داد.
3) از div خالی MoreInfoDiv، به عنوان محل افزوده شدن اطلاعات Ajax ایی دریافتی استفاده می‌کنیم.
4) دکمه بیشتر در اینجا تنها یک span ساده است که توسط css به شکل یک دکمه نمایش داده خواهد شد (فایل‌های آن در پروژه پیوست موجود است).
5) ProgressDiv در ابتدای نمایش صفحه مخفی است. زمانیکه کاربر بر روی دکمه بیشتر کلیک می‌کند، توسط افزونه جی‌کوئری ما نمایان شده و در پایان کار مجددا مخفی می‌گردد.
6) section JavaScript کار استفاده از افزونه InfiniteScroll را انجام می‌دهد.


و کدهای افزونه اسکرول نامحدود

// <![CDATA[
(function ($) {
    $.fn.InfiniteScroll = function (options) {
        var defaults = {
            moreInfoDiv: '#MoreInfoDiv',
            progressDiv: '#Progress',
            loadInfoUrl: '/',
            loginUrl: '/login',
            errorHandler: null,
            completeHandler: null,
            noMoreInfoHandler: null
        };
        var options = $.extend(defaults, options);

        var showProgress = function () {
            $(options.progressDiv).css("display", "block");
        }

        var hideProgress = function () {
            $(options.progressDiv).css("display", "none");
        }       

        return this.each(function () {
            var moreInfoButton = $(this);
            var page = 1;
            $(moreInfoButton).click(function (event) {
                showProgress();
                $.ajax({
                    type: "POST",
                    url: options.loadInfoUrl,
                    data: JSON.stringify({ page: page }),
                    contentType: "application/json; charset=utf-8",
                    dataType: "json",
                    complete: function (xhr, status) {
                        var data = xhr.responseText;
                        if (xhr.status == 403) {
                            window.location = options.loginUrl;
                        }
                        else if (status === 'error' || !data) {
                            if (options.errorHandler)
                                options.errorHandler(this);
                        }
                        else {
                            if (data == "no-more-info") {
                                if (options.noMoreInfoHandler)
                                    options.noMoreInfoHandler(this);
                            }
                            else {
                                var $boxes = $(data);
                                $(options.moreInfoDiv).append($boxes);
                            }
                            page++;
                        }
                        hideProgress();
                        if (options.completeHandler)
                            options.completeHandler(this);
                    }
                });
            });
        });
    };
})(jQuery);
// ]]>
ساختار افزونه اسکرول نامحدود به این شرح است:
هربار که کاربر بر روی دکمه بیشتر کلیک می‌کند، progress div ظاهر می‌گردد. سپس توسط امکانات jQuery Ajax، شماره صفحه (بازه انتخابی) به اکشن متد صفحه بندی اطلاعات ارسال می‌گردد. در نهایت اطلاعات را از کنترلر دریافت و به moreInfoDiv اضافه می‌کند. در آخر هم شماره صفحه را یکی افزایش داده و سپس progress div را مخفی می‌کند.

دریافت مثال و پروژه کامل این قسمت
jQueryMvcSample02.zip
 
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت نهم - مسیریابی
با سلام؛ بنده در یک پروژه Angular 5 از Routing با استفاده از outlet name استفاده کردم. با استفاده از این امکان میتونم چندین فرم رو بصورت همزمان (در Tab)  نشون بدم. برای مثال کد بنده در سمت template همچین شکلی پیدا کرده است:
<router-outlet  name="role"></router-outlet>
<router-outlet  name="product"></router-outlet>
<router-outlet  name="book"></router-outlet>
<a [routerLink]="[{ outlets: { role: ['role'] } }]">Role</a> 
<a [routerLink]="[{ outlets: { product: ['product'] } }]">Product</a>
<a [routerLink]="[{ outlets: { book: ['book'] } }]">Book</a>

این کد داخل app.component.html قرار دارد و به درستی کار میکند.
مشکلی که دارم اینه که میخوام وقتی سیستم میاد بالا بره تو فرم login به صورت تمام صفحه (که خود login یک component هست) بعدش اگه احراز هویت شد برود تو فرم اصلی که اتفاقا Tab هم آنجاست. نمیدونم خوب توضیح دادم یا نه، ولی میخوام router-outlet اصلی که بی نام هم هست یه لحظه دست login باشد بعدش تحویل بدهد به فرم اصلی سیستم که این کد    <router-outlet name="book"></router-outlet>    داخلش هست.
الان اتفاقی که میفته اینه که اگر من این کد را  <router-outlet name=" product  "></router-outlet> داخل  router-outlet اصلی داشته باشم routing‌های مانند زیر کار  نمیکنند.
<a [routerLink]="[{ outlets: { product: ['product'] } }]">Product</a>
همچنین لازم به توضیح است که نمیتونم برای login هم outlet by name استفاده کنم. چون فکر میکنم اینکارو کنم لاگین من تمام صفحه نمیشه. با تشکر
مطالب دوره‌ها
کار با فرم‌ها در بوت استرپ 3
در مطلب «استفاده از Twitter Bootstrap در کارهای روزمره طراحی وب» به نکات مرتبط با کار با فرم‌ها در بوت استرپ 2 پرداخته شد. همچنین مطالبی مانند «ویرایش قالب پیش فرض Add View در ASP.NET MVC برای سازگار سازی آن با Twitter bootstrap» برای خودکار سازی تولید فرم‌های بوت استرپ 2 در برنامه‌های ASP.NET MVC و نکات «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» نیز بررسی شدند. در بوت استرپ 3، بسیاری از این نکات تغییر کرده‌اند و نیاز است با نحوه ارتقاء فرم‌های بوت استرپ 2 به 3 و کلا نحوه کار با فرم‌ها در بوت استرپ 3 بیشتر آشنا شد.


نحوه ارتقاء فرم‌های بوت استرپ 2 به 3

تمام این تغییرات در بوت استرپ 3، جهت پیاده سازی ایده mobile-first بودن آن است. برای مثال فرم‌های افقی بوت استرپ 3 با کوچک شدن اندازه صفحه، به صورت خودکار واکنش نشان داده و تبدیل به فرم‌های معمولی که اجزای آن به صورت یک stack عمودی قرار گرفته‌اند، می‌شوند.
اکنون اگر فرم‌هایی را دارید که در برنامه‌های پیشین خود از بوت استرپ 2 استفاده کرده‌اند، نیاز است تغییرات ذیل را به آن‌ها اعمال کنید تا با سیستم جدید بوت استرپ 3 سازگار شوند:

- کلاس control-group را به کلاس form-group تبدیل کنید.
- form-search حذف شده است. آن‌را با form-inline جایگزین کنید.
- دیگر نیازی به استفاده از input-block-level نیست؛ از آنجائیکه به صورت پیش فرض کلیه inputها دارای عرض 100 درصد هستند.
- help-inline حذف شده است. آن‌را با help-block جایگزین کنید.
- عرض ستون‌ها را در فرم‌های افقی، برچسب‌ها و کنترل‌ها مشخص کنید.
- کلاس controls حذف شده است.
- کلاس form-control را به inputها و selectها اضافه کنید.
- checkboxها و radioها باید در یک div محصور شوند.
- کلاس‌های radio.inline و checkbox.inline باید با inline جایگزین شوند.
- کلاس‌های input-small به input-sm و input-large به input-lg تبدیل شده‌اند.
- کلاس‌های input-prepend با input-group و input-append با input-group جایگزین شده‌اند.
- کلاس alert-error حذف شده‌است. بجای آن می‌شود از alert-warning استفاده کرد.
- کلاس alert-block را با alert جایگزین کنید.


ایجاد اولین فرم افقی با بوت استرپ 3

فرض کنید که قصد داریم یک چنین فرم افقی را توسط امکانات بوت استرپ 3 ایجاد کنیم:



همانطور که ملاحظه می‌کنید، با کوچک شدن اندازه صفحه، این فرم نیز تغییر شکل می‌دهد:



کدهای کامل این فرم را در ادامه ملاحظه می‌کنید:
    <div class="container">
        <h4 class="alert alert-info">
            فرم‌های بوت استرپ 3</h4>
        <div class="row">
            <article class="registrationform">
                <h2>
                    فرم ثبت نام</h2>               
                <form class="registration form-horizontal" action="#">
                <fieldset id="personalinfo">
                    <legend>اطلاعات شخصی</legend>
                    <section class="row">
                        <label class="col col-lg-4 control-label" for="myname">
                            نام</label>
                        <div class="controls">
                            <input class="col col-lg-8" type="text" name="myname" 
                                   id="myname" autofocus placeholder="نام و نام خانوادگی"
                                   required>
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                    <section class="row">
                        <label class="col col-lg-4 control-label" for="companyname">
                            نام شرکت</label>
                        <div class="controls">
                            <input class="col col-lg-8" type="text" name="companybname" id="companyname" />
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                    <section class="row">
                        <label class="col col-lg-4 control-label" for="myemail">
                            ایمیل</label>
                        <div class="controls">
                            <input class="col col-lg-8" type="email" name="myemail" id="myemail" 
                                   required autocomplete="off" />
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                </fieldset>
                <!-- personal info -->
                <fieldset id="otherinfo">
                    <legend>سایر اطلاعات</legend>
                    <section class="row">
                        <label class="col col-lg-4 control-label">
                            نوع درخواست</label>
                        <div class="controls col col-lg-8">
                            <label class="radio">
                                <input type="radio" name="requesttype" value="question" />
                                سؤال
                            </label>
                            <label class="radio">
                                <input type="radio" name="requesttype" value="comment" />
                                انتقاد
                            </label>
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                    <section class="row">
                        <label class="col col-lg-4 control-label">
                            خبرنامه</label>
                        <div class="controls col col-lg-8">
                            <label class="checkbox">
                                <input type="checkbox" id="subscribe" name="subscribe" checked value="yes" />
                               آیا مایل به دریافت ایمیل‌های خبرنامه ما هستید؟
                            </label>
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                    <section class="row">
                        <label class="col col-lg-4 control-label" for="reference">
                            چطور از وجود سایت ما آگاه شدید؟</label>
                        <div class="controls col col-lg-8">
                            <select name="reference" id="reference">
                                <option>لطفا انتخاب کنید...</option>
                                <option value="friend">از طریق یک دوست</option>
                                <option value="facebook">Facebook</option>
                                <option value="twitter">Twitter</option>
                            </select>
                        </div>
                        <!-- controls -->
                    </section><!-- row -->
                </fieldset>
                <button class="btn" type="submit">
                    ارسال</button>
                </form>
            </article>
        </div>
        <!-- end row -->
    </div>
    <!-- /container -->
توضیحات:

- باید درنظر داشت که اگر هیچگونه فرمتی را به فرم‌های بوت استرپ 3 اعمال نکنیم، به صورت پیش فرض فرمت دهی شده و تبدیل به فرم‌های عمودی شکیلی می‌شوند که شاید از دیدگاه خیلی‌ها مناسب بوده و نیاز به تغییرات خاصی نداشته باشند.
- برای تبدیل این فرم عمودی پیش فرض، به فرم‌های افقی دو ستونه، نیاز است یک سری کلاس بوت استرپ 3 را به المان‌های آن اضافه کنیم. برای این منظور ابتدا کلاس form-horizontal را به تگ فرم اضافه می‌کنیم.
- هر سطر فرم، در یک المان section با کلاس row قرار خواهد گرفت.
- اکنون هر سطر، از یک برچسب به همراه یک یا چند المان تشکیل خواهد شد. در هر سطر، کنترل‌ها در یک div با کلاس controls قرار می‌گیرند.
- برای اینکه برچسب‌های هر ردیف با کنترل‌ها و المان‌های آن ردیف، تراز شوند، تنها کافی است به آن‌ها کلاس control-label را اضافه کنیم.
در ادامه تمام این مراحل را باید به ازای هر سطر فرم تکرار کنیم.

- زمانیکه به radio buttons یا check boxes می‌رسیم، باید به چند نکته دقت داشت:
الف) حین کار با radio buttons، علاوه بر برچسب آن سطر که با label مشخص می‌شود، هر radio button نیز باید داخل یک label با کلاس radio محصور شود.
ب) تمام radio buttons یک سطر نیز باید در یک div ایی با کلاس controls محصور شوند.
این نکته در مورد check boxes نیز صادق است.

با رعایت همین چند نکته ساده می‌توان به یک طراحی دو ستونی خودکار واکنشگرا رسید.



اصلاح قالب ایجاد فرم‌های خودکار ASP.NET MVC بر اساس بوت استرپ 3

مطلب «ویرایش قالب پیش فرض Add View در ASP.NET MVC برای سازگار سازی آن با Twitter bootstrap» جهت بوت استرپ 2 تهیه شده بود. فایل نهایی ویرایش شده آن‌را با توجه به توضیحات مطلب جاری برای بوت استرپ 3 از پیوست انتهای بحث دریافت کنید و برای استفاده از آن فقط کافی است آن‌را در مسیر CodeTemplates\AddView\CSHTML\CreateBootstrap3Form.tt ریشه پروژه جاری خود کپی و به پروژه اضافه کنید تا در صفحه دیالوگ Add view ظاهر شود (خاصیت custom tool آن‌را هم خالی کنید).


در مورد اعتبارسنجی‌های فرم‌ها چطور؟

اصلاح مطالبی مانند «اعمال کلاس‌های ویژه اعتبارسنجی Twitter bootstrap به فرم‌های ASP.NET MVC» جهت کار با فرم‌های بوت استرپ 3 بسیار ساده است. از این جهت که در کدهای آن فقط باید نام کلاس‌های CSS قدیمی به جدید ویرایش شوند. مابقی کدها یکسان است. مثلا نام کلاس control-group شده است form-group (همان توضیحات ابتدای بحث جاری). کلاس‌های error شده‌اند has-error و success شده است has-success.



فایل‌های نهایی این قسمت را از اینجا نیز می‌توانید دریافت کنید:
bs3-sample05.zip  
مطالب
روش نصب اندروید 10 بر روی گوشی‌های قدیمی سامسونگ
چند سال قبل یک گوشی Samsung galaxy J7 را که به صورت پیش‌فرض به همراه اندروید 6 است، تهیه کردم. الان حدود دو هفته‌ای است که اندروید 10 را از طریق LineageOS بر روی این گوشی نصب کرده‌ام که در ادامه روش نصب آن‌را برای علاقمندان توضیح خواهم داد.


LineageOS چیست؟

یکی از مهم‌ترین مزایای استفاده از گوشی‌های اندرویدی نسبت به iOS ای، آزادی نصب نرم افزار و خصوصا سیستم عامل‌های مختلف بر روی آن‌ها است. در حال حاضر، محبوب‌ترین و پر استفاده‌ترین نگارش آزاد اندروید که مستقل از گوگل عمل می‌کند، LineageOS نام‌دارد (لی‌نی‌ایج OS) که پیشتر با نام‌های CyanogenMod و قبل از آن، Cyanogen (سیانوژن) ارائه می‌شد. یکی از مهم‌ترین مزایای آن، امکان نصب آخرین نگارش اندروید بر روی گوشی‌هایی است که دیگر پشتیبانی رسمی نمی‌شوند و خط تولید آن‌ها خاتمه یافته‌است.
به LineageOS یک Custom ROM هم گفته می‌شود. ROM مخفف read-only memory است و دقیقا جائی‌است که هسته‌ی Android در آن مشغول به کار است. بنابراین منظور از Custom ROM، همان نگارش سفارشی از Android است. به عملیات نصب LineageOS در اصطلاح Flashing هم گفته می‌شود که به معنای بازنویسی قسمتی از یک نرم‌افزار، با نرم‌افزار دیگری است.


پیشنیازهای ضروری نصب LineageOS

- داشتن یک گوشی یا تبلت سازگار با آن (متاسفانه سایت lineageos.org با IP ایرانی باز نمی‌شود)
- دسترسی به یک کابل USB مخصوص گوشی
- داشتن یک کامپیوتر دسکتاپ و یا لپ‌تاپ
- دسترسی به اینترنت
- زمان! ... (انجام این عملیات برای من در بار اول، حدودا یک روز طول کشید! (صرف نظر از تحقیقات یک هفته‌ای روش انجام آن) البته نه به علت طولانی بودن زمان نصب آن، بلکه به علت وجود نکات ریزی که در هیچ مستنداتی، به صورت مدون پیدا نخواهید کرد و عدم آشنایی با آن‌ها ممکن است سبب بروز حمله‌ی قلبی، به علت در دست داشتن سخت افزاری شود که هم اکنون کل آن‌را فرمت کرده‌اید و ... راهنماهای ارائه شده‌ی در اینترنت هم بر روی آن کار نمی‌کنند! به یک چنین سخت افزاری، brick یا «پاره آجر» هم گفته می‌شود!)


دریافت Custom ROM سازگار با گوشی یا تبلت

مرحله‌ی اول نصب LineageOS، دریافت Custom ROM آن است. برای این منظور به آدرس download.lineageos.org مراجعه کرده و ابتدا از منوی سمت چپ صفحه، گوشی خود را پیدا کنید و سپس با انتخاب آن، امکان دریافت ROM مخصوص آن‌را خواهید یافت.

نکته‌ی مهم! متاسفانه در اولین دریافت من از این سایت، به علت ناقص بودن دانلود، فایل دریافتی به همراه CRC Error بود و در زمان نصب فایل zip آن، خطای کلی e1001 ظاهر شد و نه هیچ چیز دیگری. این لحظه واقعا لحظه‌ای است که ممکن است عرق سرد بر روی پیشانی شما ظاهر شود! به صورت اتفاقی با بررسی فایل zip آن بر روی کامپیوتر متوجه شدم که فایل، ناقص دریافت شده. به همین جهت پیش از شروع به نصب، فایل zip را در یک برنامه‌ی باز کننده‌ی آن‌ها مانند winrar و یا 7-zip باز کرده و بر روی دکمه‌ی test آن‌ها کلیک کنید. اگر خطایی را گزارش ندادند، شروع به ادامه‌ی مراحل نصب کنید.


دریافت فایل Recovery سفارشی

در اینجا نیاز است با دو واژه‌ی جدید bootloader و recovery آشنا شد. زمانیکه گوشی خودتان را روشن می‌کنید، اولین نرم افزاری که حتی پیش از سیستم عامل اجرا می‌شود، bootloader نام دارد که کار آن آغاز سایر پروسه‌ها است. بعد از بارگذاری بوت‌لودر، برنامه‌ی دیگری به نام recovery، کار بارگذاری سیستم عامل را انجام می‌دهد. بوت‌لودر و recovery پیش‌فرض اندروید، اجازه‌ی نصب یک custom ROM را نمی‌دهند. به همین جهت نیاز است این برنامه‌ی recovery را با یک نمونه‌ی سفارشی بازنویسی کرد که این نمونه‌ی سفارشی در اینجا TWRP نام دارد و نمونه‌ی مخصوص گوشی خود را می‌توانید با جستجوی در لیست https://twrp.me/Devices دریافت کنید. ابتدا نوع گوشی و سپس مدل آن‌را یافته و سپس در صفحه‌ای که ظاهر می‌شود، بر روی download link آن کلیک کنید تا لیست فایل‌های موجود ظاهر شوند. در ابتدا آخرین نگارش موجود را دریافت کنید.

یک تجربه! متاسفانه آخرین نگارش TWRP دریافت شده، بر روی گوشی من کار نکرد و پس از نصب آن، مدام وارد همان سیستم عامل قبلی، با ارائه‌ی پیام «Recovery is NOT SEANDROID Enforcing» می‌شد و هیچ تاثیری را نداشت. در این حالت نصب نگارش قدیمی‌تر 3.3.1، کار کرد. بنابراین بهتر است چندین نگارش آن‌را دریافت کنید؛ تا در صورت لزوم بتوانید یکی یکی، آن‌ها را آزمایش کنید.


دریافت Google Apps

LineageOS به همراه برنامه‌های گوگل، مانند play store و امثال این‌ها نیست. به همین جهت نیاز است آن‌ها را از آدرس https://opengapps.org دریافت کنید. در اینجا دقت داشته باشید که چه چیزی را انتخاب می‌کنید! برای نمونه برای گوشی من گزینه‌های ARM، نگارش 10 و pico انتخاب شدند و سپس کلیک بر روی دکمه‌ی دانلود. گزینه‌ی pico، یکی از کم حجم‌ترین نگارش‌ها است و همینقدر برای شروع به کار، کافی است. نگارش را 10 انتخاب می‌کنیم چون می‌خواهیم اندروید 10 را نصب کنیم و انتخاب معماری CPU گوشی هم مهم است. با استفاده از برنامه‌ای مانند device info، به برگه‌ی CPU آن مراجعه کرده و CPU Type گوشی خود را دقیق بررسی کنید. اگر مانند گوشی من، 32bit بود، باید ARM را انتخاب کنید و اگر 64bit بود، گزینه‌ی ARM64 را انتخاب کنید و اگر یک گوشی قدیمی را مانند ASUS دارید، ممکن است CPU آن از نوع intel و x86 باشد.



دریافت برنامه‌ی فعالسازی دسترسی root

اگر می‌خواهید دسترسی root هم داشته باشید (این گزینه اختیاری است و من آن‌را نصب نکردم)، در نگارش‌های قبلی LineageOS از برنامه‌ای به نام SU برای انجام اینکار استفاده می‌شد. این برنامه دیگر نگهداری نمی‌شود و نباید آن‌را به همراه آخرین نگارش LineageOS نصب کرد (خیلی مهم!)؛ وگرنه گوشی شما را حتما به هم خواهد ریخت. برنامه‌ی جایگزین آن Magisk نام دارد که باز هم من آن‌را توصیه نمی‌کنم! چون اگر به انجمن‌های LineageOS مراجعه کنید، مشاهده شده‌است که پس از نصب به روز رسانی‌های جدید هفتگی LineageOS، ممکن است به علت عدم سازگاری با Magisk، سیستم عامل گوشی شما بالا نیاید و در یک حلقه‌ی بی‌پایان قرار بگیرید. به همین جهت بهتر است از این گزینه صرفنظر کنید.



آماده سازی گوشی برای اتصال USB و اجرای فرامین بر روی آن

مرحله‌ی بعد، نصب برنامه‌ی recovery سفارشی است. برای اینکار نیاز است گوشی خود را توسط سیم USB، به یک کامپیوتر متصل کرده و سپس توسط برنامه‌ای خاص که در ادامه معرفی می‌شود، برنامه‌ی TWRP را بر روی آن نصب کرد. به همین جهت به قسمت «تنظیمات» گوشی اندرویدی خود رفته و گزینه‌ی «درباره‌ی دستگاه (About)» را پیدا کنید. سپس بر روی شماره‌ی build آن «Build Number»، هفت بار ضربه بزنید. اینکار سبب می‌شود تا یک منوی مخفی به نام «Developer Mode» یا «گزینه‌های توسعه دهندگان/برنامه نویسان»، به لیست منوهای تنظیمات سیستم عامل فعلی اضافه شود. پس از فعال شدن «Developer Mode»، به این گزینه وارد شده و دو گزینه‌ی زیر را در آن فعال کنید:
- USB debugging
- OEM unlocking

اکنون اگر گوشی خود را از طریق سیم USB به کامپیوتر متصل کنید، یک دیالوگ باکس پرسشی، در اندروید جاری ظاهر می‌شود که درخواست دسترسی به ADB را از شما سؤال می‌پرسد. گزینه‌ی «Always Allow From This Computer» را انتخاب کرده و با کلیک بر روی OK، این دسترسی را فعال کنید.



دریافت برنامه‌های انتقال اطلاعات به گوشی اندرویدی

پس از دریافت فایل‌های مورد نیاز (TWRP.img, firmware.zip و gapps.zip)، اکنون نوبت به نصب TWRP.img است تا برنامه‌ی recovery پیش‌فرض گوشی را با یک نمونه‌ی سفارشی که امکان نصب custom ROM را میسر می‌کند، بازنویسی کنیم. بر روی گوشی‌های سامسونگ، برنامه‌ی ODIN یک چنین قابلیتی را به همراه دارد.


البته اگر کمی جستجو کنید، به دستورات زیر هم خواهید رسید که توسط برنامه‌ی Minimal_ADB_Fastboot قابل اجرا هستند:
adb devices
adb reboot bootloader
fastboot devices
fastboot flash recovery TWRP.img
fastboot reboot-bootloader
من تمام این دستورات را آزمایش کردم و بر روی گوشی سامسونگ کار نکردند! اما ODIN کار کرد.
البته برنامه‌ی Minimal_ADB_Fastboot، برنامه‌ی بسیار مفیدی است و در ادامه کاربردهایی از آن‌را مطالعه خواهید کرد.


بررسی امنیتی مهم! آیا فایل ROM دریافت شده، بر روی گوشی من نصب می‌شود؟!

در ادامه پیش از نصب، یکبار گوشی را فرمت می‌کنیم. در این حال اگر در حین نصب، پیام سازگار نبودن فایل ROM را دریافت کنیم، بسیار دیر است! به همین جهت پس از نصب برنامه‌ی Minimal_ADB_Fastboot، به پوشه‌ی آن وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید (با این فرض که گوشی شما از طریق سیم USB به کامپیوتر متصل است و همچنین دسترسی دیباگی را هم که در گوشی عنوان شد، داده‌اید):
adb devices
adb shell getprop ro.product.device
adb shell getprop ro.build.product
دستور اول، adb server را اجرا کرده و سیستم شما را به گوشی متصل می‌کند. همچنین یک id را هم نمایش می‌دهد که نشان از موفقیت آمیز بودن اتصال دارد. دو دستور بعدی، شماره دستگاه و مدل آن‌را بازگشت می‌دهند. این خروجی‌ها را به دقت بخاطر بسپرید.
سپس فایل custom ROM دریافت شده را باز کرده و به پوشه‌ی «META-INF\com\google\android» آن وارد شوید. در اینجا فایل متنی updater-script را باز کنید. برای مثال در مورد گوشی من، چنین سطری در ابتدای آن درج شده:
assert(getprop("ro.product.device") == "j7elte" || getprop("ro.build.product") == "j7elte"
|| abort("E3004: This package is for device: j7elte; this device is " + getprop("ro.product.device") + "."););
این سطر، دقیقا بررسی می‌کند که اگر خاصیت‌های ro.build.product یا ro.product.device مساوی j7elte نبودند، کل عملیات نصب، abort شود.
بنابراین حتما پیش از مطالعه و اجرای ادامه‌ی بحث، مقادیر این ویژگی‌ها را با سطر اول فایل updater-script انطباق دهید تا اگر یکی نبودند، به اشتباه گوشی خود را فرمت نکنید!
البته در جائی دیدم که عده‌ای برای «خوراندن» rom سفارشی دریافت شده، این سطر بررسی را از فایل یاد شده، پاک کرده و سپس فایل zip جدیدی را تولید و نصب کرده‌اند. بهتر است اینکار را نکنید و با جستجوی دقیق مطمئن شوید که یک چنین تغییری، برای سیستم شما مشکلی را ایجاد نمی‌کند!


بازنویسی برنامه‌ی recovery گوشی توسط ODIN

پس از دریافت برنامه‌ی odin، نیاز است گوشی خود را خاموش کنید. فرض بر این است که پیشتر حداقل از contacts خود پشتیبان تهیه کرده‌اید. چون از این قسمت به بعد، به مراحل بدون بازگشتی قدم خواهیم گذاشت و قرار است گوشی را کاملا فرمت کنیم!
پس از خاموش کردن گوشی، اکنون نیاز است گوشی را در حالت download بالا بیاوریم. برای اینکار سه دکمه‌ی Volume Down + Home + Power با هم بفشارید. بنابراین ابتدا دکمه‌ی «کاهش صدا» را نگه دارید و رها نکنید، سپس دکمه‌ی home را نگه دارید و رها نکنید و در آخر دکمه‌ی power را نگه دارید تا گوشی به حالت ویژه‌ی download وارد شود.
البته در ابتدا یک صفحه‌ی اخطار را نمایش می‌دهد که در آن درج شده برای ادامه نیاز است دکمه‌ی «افزایش صدا» را بفشارید.


پس از ظاهر شدن تصویر فوق، اینبار دکمه‌ی «افزایش صدا» را بفشارید تا وارد حالت دانلود شوید. در اینجا «حالت دانلود» یعنی گوشی قابلیت دریافت فایلی را پیدا کرده‌است.


پس از بالا آوردن گوشی در حالت دانلود، برنامه‌ی odin را باز کنید:


- در اینجا در قسمت AP، فایل tar مربوط به TWRP را انتخاب کنید.
- سپس در برگه‌ی options، تیک گزینه‌ی Auto reboot را بردارید (بسیار مهم!). اگر این تیک را برندارید، پس از کار نوشتن برنامه‌ی recovery سفارشی، گوشی شما reboot شده و ... وارد برنامه‌ی recovery ... نمی‌شود! چون سیستم امنیتی توکار اندروید، آن‌را با نمونه‌ی اصلی جایگزین می‌کند!
- اکنون بر روی دکمه‌ی start کلیک کنید تا کار بازنویسی شروع شود.

پس از پایان بازنویسی برنامه‌ی recovery، باید وارد این برنامه‌ی جدید بشویم که روش ورود به آن به صورت زیر است:
پس از پایان بازنویسی، در بعضی از گوشی‌ها در همین حالت که گوشی، حالت download را نمایش می‌دهد، اگر ترکیب کلید‌های «Volume Up + Power + Home» را بفشارید (اینبار دکمه‌ی «افزایش صدا» است و نه کاهش صدا)، وارد این برنامه‌ی recovery جدید می‌شوید. اما در مورد گوشی من چنین چیزی رخ نداد. در این حالت تنها روشی که پاسخ داد، «خارج کردن باطری گوشی» بود (در همین حالتی که صفحه‌ی آبی رنگ download نمایش داده می‌شود، باطری را خارج کنید)؛ چون حتی در حالت خاموش کردن معمولی هم برنامه‌ی recovery سفارشی را پاک و نمونه‌ی اصلی را جایگزین می‌کرد!
سپس سیستم را به صورت معمولی روشن نکنید. اینبار نیاز است وارد منوی recovery شویم. بنابراین مجددا باطری را قرار داده و اینبار با فشردن ترکیب کلید‌های «Volume Up + Power + Home» به منوی جدید recovery وارد خواهیم شد.


مرحله‌ی آخر! نصب سیستم عامل جدید و برنامه‌های گوگل

تا اینجا باید وارد منوی recovery جدید شده باشید. روش خارج کردن باطری را هم فراموش نکنید! (چون اگر سیستم به صورت معمولی ری‌استارت شود، یا حتی به صورت معمولی خاموش شود، برنامه‌ی recovery سفارشی را بی‌اثر کرده و پاک می‌کند)
- پس از بارگذاری برنامه، پیام «Swipe to Allow Modifications» ظاهر می‌شود. برای این منظور، فلش آبی رنگ ظاهر شده را به سمت راست بکشید تا بتوانید وارد برنامه شوید.
- اکنون این مراحل را طی کنید:

الف) انتخاب Wipe


در اینجا در ابتدا گزینه‌ی Format Data را انتخاب کنید.


سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار فرمت کردن سیستم شروع شود.
در ادامه در همین قسمت گزینه‌ی Advanced Wipe را انتخاب کرده (همیشه با انتخاب دکمه‌ی back می‌توان به منوی اصلی و گزینه‌های آن رسید) و Dalvik / ART Cache,Data, System, Cache, Internal storage را انتخاب کنید. سپس مجددا فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار پاک کردن سیستم شروع شود. در اینجا همه چیز را منهای SD Card، پاک خواهیم کرد. بدون انجام اینکار، وارد یک حلقه‌ی بی‌نهایت خواهید شد و سیستم اصلی پس از نصب، راه اندازی نمی‌شود (آزمایش کردم!).


ب) انتقال فایل‌های Custom ROM و GApps به گوشی

اکنون به کامپیوتر خود و پوشه‌ی محل نصب برنامه‌ی Minimal_ADB_Fastboot وارد شده و خط فرمان را در آنجا آغاز کنید. برای این منظور فقط کافی است بر روی فایل cmd-here.exe کلیک کنید. سپس فرامین زیر را اجرا کنید تا فایل‌ها به گوشی منتقل شوند:
adb devices
adb push LINEAGE.zip /sdcard/
adb push GAPPS.zip /sdcard/
دو نکته:
1- فایل‌های custom ROM و GApps دریافت شده را به درون پوشه‌ی Minimal_ADB_Fastboot کپی کنید. در اینجا منظور از LINEAGE.zip نام کاملی مانند lineage-17.1-20210114-nightly-j7elte-signed.zip است که دریافت کرده‌اید و همچنین منظور از GAPPS.zip، نام کاملی مانند open_gapps-arm-10.0-pico-20210116.zip است.
2- برای اجرای این دستورات، نیازی به داشتن یک sdcard نیست. نامی که در اینجا ذکر شده، فقط یک نام پوشه‌ی جدید، در گوشی شما است که قرار است در ادامه فایل‌ها را از آن انتخاب کنیم.

یک نکته‌ی تکمیلی: در حالت منوی recovery و بعد از پاک کردن همه چیز، اگر فولدرهای گوشی در windows explorer مشخص نیستند، باید آن‌ها را mount کرد تا بشود فایل‌ها را به آن‌ها کپی کرد (روش دوم کپی کردن فایل‌ها به گوشی). اگر به منوی ابتدایی TWRP دقت کنید، یک گزینه‌ی mount هم دارد که دقیقا برای همین منظور است. پوشه‌ها را که mount کردید، در windows explorer جهت کپی کردن معمولی ظاهر می‌شوند.


ج) نصب نهایی سیستم عامل و برنامه‌های گوگل

پیش از هر کاری به گزینه‌ی Settings در برنامه‌ی TWRP مراجعه کرده و در برگه‌ی General آن، تیک زیر را بر دارید: Prompt to install TWRP app if not installed!

اکنون که فایل‌های custom ROM و GApps به گوشی کپی شدند، از منوی اصلی TWRP، اینبار گزینه‌ی Install را انتخاب کنید (همانطور که عنوان شد، در اینجا همیشه دکمه‌ی back، برای بازگشت به صفحه‌ی اصلی کار می‌کند).


اگر از طریق دستورات adb فایل‌ها را به پوشه‌ی sdcard منتقل کرده باشید، به صورت خودکار اولین فایل انتخاب شده همان فایل ROM است. سپس بر روی دکمه‌ی «Add more zips» کلیک کرده و فایل zip مربوط به GApps را انتخاب کنید. در بالای صفحه «two of max 10 File queued» باید ظاهر شده باشد (مهم) که به معنای تعداد فایل‌های موجود در صف نصب است. اکنون فلش آبی رنگ پایین صفحه را به سمت راست بکشید تا کار نصب شروع شود.
پس از پایان نصب این دو برنامه، یکبار بر روی دکمه‌ی Wipe cache/dalvik کلیک کنید (به همراه به سمت راست کشیدن دکمه‌ی فلش آبی پایین صفحه) و سپس بر روی دکمه‌ی Reboot System تا ... وارد  اندروید 10 شوید!

یک نکته: در اینجا در حین reboot سؤال می‌پرسد که آیا نیاز است TWRP را نیز به صورت جداگانه‌ای نصب کند. عنوان کنید، خیر.


چگونه به روز رسانی‌های LineageOS را نصب کنیم؟

LineageOS هفته‌ای یکبار، آخرین به روز رسانی‌های اندروید را توزیع می‌کند. برای نصب آن‌ها پیامی را ظاهر کرده و امکان دانلود را فراهم می‌کند. پس از دانلود، اگر بر روی دکمه‌ی install کلیک کنید، به صورت خودکار شما را وارد منوی recovery فوق می‌کند (و نه نصب خودکار). در اینجا تنها کاری را که باید انجام دهید، انتخاب گزینه‌ی install است و سپس انتخاب پوشه‌ی data/lineageos_updates که محل قرار گیری این فایل zip دریافت شده‌است. با انتخاب فایل zip، مراحل نصب آن مانند قبل است. پس از پایان نصب، یکبار بر روی دکمه‌ی پاک کردن کش dalvik کلیک کنید و سپس بر روی reboot. کش dalvik همواره به صورت خودکار توسط اندروید ساخته می‌شود و پاک کردن آن مشکلی را ایجاد نمی‌کند.
پس از راه اندازی مجدد سیستم، به منوی Settings>about phone>lineageOS مراجعه کرده و فایل zip قدیمی را حذف کنید (در همان صفحه‌ای که پیام دریافت و نصب را نمایش می‌داد، اکنون پیام delete ظاهر شده‌است).
مطالب
معرفی پلاگین Agent Johnson !

چقدر خوب می‌شد اگر IDE ویژوال استودیو دات نت اشیایی از نوع disposable را برای ما مشخص می‌کرد و در ادامه پیشنهاد استفاده از عبارت using را برای آن‌ها می‌داد؟
سؤال: این مساله چه اهمیتی دارد؟
به مثال متداول زیر دقت بفرمائید:

string connString = "...";
SqlConnection conn = new SqlConnection(connString);
string cmdString = "select * from author where name=N'test'";
SqlCommand cmd = new SqlCommand(cmdString, conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while(reader.Read())
{
...
}
conn.Close();

این مثالی است که در اکثر سایت‌های آموزشی می‌توان یافت. باز کردن کانکشن به دیتابیس، اجرای کوئری و دریافت اطلاعات حاصل و در آخر بستن کانکشن. بسیار هم خوب! در این مثال اگر name مساوی test وجود نداشته باشد، روند اجرای برنامه در اولین reader.Read با یک exception متوقف خواهد شد. یعنی عملا ممکن است به بستن کانکشن نرسیم (هیچ الزامی وجود ندارد که کدهای ما سطر به سطر اجرا شوند). این مورد چه مشکلی را ایجاد خواهد کرد؟ فقط کافی است سایت ما 100 نفر بازدید کننده داشته باشد. در مدت زمان کوتاهی کل سایت با خطای زیر از کار می‌افتد و پیغامی که از کاربران دریافت خواهید کرد این است: "باز هم برنامه کند شد، "بازهم" سایت کار نمی‌کنه ..."
Exception Details: System.InvalidOperationException: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.

روش صحیح برای مقابله با این نوع خطاها قرار دادن قسمت بستن کانکشن در قسمت finally یک try/finally است تا مطمئن باشیم که حتما این قسمت اجرا خواهد شد. روش زیباتر این‌کار استفاده از عبارت using است. عبارت using نیز در نهایت به یک try/finally توسط کامپایلر بسط داده خواهد شد اما کد حاصل در این حالت خواناتر است. بنابراین در این حالت حتی اگر خطایی نیز رخ دهد، شیء مورد نظر حتما dispose خواهد شد. (از دات نت 2 به بعد عبارت using به VB.Net نیز افزوده شده است)

 using (SqlConnection connection =
new SqlConnection(connectionString))
{
...

عبارت using را تنها در مورد اشیایی از نوع IDisposable می‌توان بکار برد. یکی از توانایی‌های پلاگین Agent Johnson تشخیص این مورد و گوشزد کردن آن است (شکل زیر):



Agent Johnson در حقیقت پلاگینی است برای پلاگین دیگری به نام Resharper . این پلاگین را از آدرس زیر می‌توان دریافت کرد (احتمالا به یک پروکسی نیاز خواهد بود ...):
http://code.google.com/p/agentjohnsonplugin/
البته بحث تشخیص اشیاء disposable یکی از توانایی‌های آن است. سایر موارد را در آدرس فوق می‌توانید مشاهده نمائید.

بازخوردهای دوره
شروع به کار با RavenDB
« ابتدا این ساختار در بانک تشکیل میشود  »
خیر. این فقط ساختار یک سند است. سند بعدی را هر طور که علاقمند بودید طراحی و ثبت کنید. متد session.Store محدودیتی ندارد. همچنین جایی هم در برنامه این ساختار در ابتدای کار به بانک اطلاعاتی معرفی یا ثبت نمی‌شود. وجود یک کلاس در برنامه به معنی تشکیل ساختار آن در بانک اطلاعاتی نیست.
بدون اسکیما یعنی هر رکورد با رکورد قبلی یا بعدی خودش می‌تواند ساختار کاملا متفاوتی داشته باشد.
مطالب
VS Code برای توسعه دهندگان ASP.NET Core - قسمت دوم - ایجاد و اجرای اولین برنامه
پس از معرفی ابتدایی VSCode و نصب افزونه‌ی #C در قسمت قبل، در ادامه می‌خواهیم اولین پروژه‌ی ASP.NET Core خود را در آن ایجاد کنیم.


نصب ASP.NET Core بر روی سیستم عامل‌های مختلف

برای نصب پیشنیازهای کار با ASP.NET Core به آدرس https://www.microsoft.com/net/download/core مراجعه کرده و NET Core SDK. را دریافت و نصب کنید. پس از نصب آن جهت اطمینان از صحت انجام عملیات، دستور dotnet --version را در خط فرمان صادر کنید:
 C:\>dotnet --version
1.0.1
در اینجا SDK نصب شده، شامل هر دو نگارش 1.0 و 1,1 است. همچنین در همین صفحه‌ی دریافت فایل‌ها، علاوه بر نگارش ویندوز، نگارش‌های Mac و لینوکس آن نیز موجود هستند. بر روی هر کدام که کلیک کنید، ریز مراحل نصب هم به همراه آن‌ها وجود دارد. برای مثال نصب NET Core. بر روی Mac شامل نصب OpenSSL به صورت جداگانه است و یا نصب آن بر روی لینوکس به همراه چند دستور مختص به توزیع مورد استفاده می‌باشد که به خوبی مستند شده‌اند و نیازی به تکرار آن‌ها نیست و همواره آخرین نگارش آن‌ها بر روی سایت dot.net موجود است.


ایجاد اولین پروژه‌ی ASP.NET Core توسط NET Core SDK.

پس از نصب NET Core SDK.، به پیشنیاز دیگری برای شروع به کار با ASP.NET Core نیازی نیست. نه نیازی است تا آخرین نگارش ویژوال استودیوی کامل را نصب کنید و نه با به روز رسانی آن، نیاز است چندگیگابایت فایل به روز رسانی تکمیلی را دریافت کرد. همینقدر که این SDK نصب شود، می‌توان بلافاصله شروع به کار با این نگارش جدید کرد.
در ادامه ابتدا پوشه‌ی جدید پروژه‌ی خود را ساخته (برای مثال در مسیر D:\vs-code-examples\FirstAspNetCoreProject) و سپس از طریق خط فرمان به این پوشه وارد می‌شویم.

یک نکته: در ویندوزهای جدید فقط کافی است در نوار آدرس بالای صفحه تایپ کنید cmd. به این صورت بلافاصله command prompt استاندارد ویندوز در پوشه‌ی جاری در دسترس خواهد بود و دیگر نیازی نیست تا چند مرحله را جهت رسیدن به آن طی کرد.

پس از وارد شدن به پوشه‌ی جدید از طریق خط فرمان، دستور dotnet new --help را صادر کنید:
D:\vs-code-examples\FirstAspNetCoreProject>dotnet new --help
Getting ready...
Template Instantiation Commands for .NET Core CLI.

Usage: dotnet new [arguments] [options]

Arguments:
  template  The template to instantiate.

Options:
  -l|--list         List templates containing the specified name.
  -lang|--language  Specifies the language of the template to create
  -n|--name         The name for the output being created. If no name is specified, the name of the current directory is used.
  -o|--output       Location to place the generated output.
  -h|--help         Displays help for this command.
  -all|--show-all   Shows all templates


Templates                 Short Name      Language      Tags
----------------------------------------------------------------------
Console Application       console         [C#], F#      Common/Console
Class library             classlib        [C#], F#      Common/Library
Unit Test Project         mstest          [C#], F#      Test/MSTest
xUnit Test Project        xunit           [C#], F#      Test/xUnit
ASP.NET Core Empty        web             [C#]          Web/Empty
ASP.NET Core Web App      mvc             [C#], F#      Web/MVC
ASP.NET Core Web API      webapi          [C#]          Web/WebAPI
Solution File             sln                           Solution

Examples:
    dotnet new mvc --auth None --framework netcoreapp1.1
    dotnet new xunit --framework netcoreapp1.1
    dotnet new --help
همانطور که مشاهده می‌کنید، اینبار بجای انتخاب گزینه‌های مختلف از صفحه دیالوگ new project template داخل ویژوال استودیوی کامل، تمام این قالب‌ها از طریق خط فرمان در اختیار ما هستند. برای مثال می‌توان یک برنامه کنسول و یا یک کتابخانه‌ی جدید را ایجاد کرد.
در ادامه دستور ذیل را صادر کنید:
 D:\vs-code-examples\FirstAspNetCoreProject>dotnet new mvc --auth None
به این ترتیب یک پروژه‌ی جدید ASP.NET Core، بدون تنظیمات اعتبارسنجی و ASP.NET Core Identity، در کسری از ثانیه ایجاد خواهد شد.


سپس جهت گشودن این پروژه در VSCode تنها کافی است دستور ذیل را صادر کنیم:
 D:\vs-code-examples\FirstAspNetCoreProject>code .
در ادامه یکی از فایل‌های #C آن‌را گشوده و منتظر شوید تا کار دریافت خودکار بسته‌های مرتبط با افزونه‌ی #C ایی که در قسمت قبل نصب کردیم به پایان برسند:


اینکار یکبار باید انجام شود و پس از آن امکانات زبان #C و همچنین دیباگر NET Core. در VS Code قابل استفاده خواهند بود.
در تصویر فوق دو اخطار را هم مشاهده می‌کنید. مورد اول برای فعال سازی دیباگ و ساخت پروژه‌ی جاری است. گزینه‌ی Yes آن‌را انتخاب کنید و اخطار دوم جهت بازیابی بسته‌های نیوگت پروژه‌است که گزینه‌ی restore آن‌را انتخاب نمائید. البته کار بازیابی بسته‌ها از طریق کش موجود در سیستم به سرعت انجام خواهد شد.


گشودن کنسول از درون VS Code و اجرای برنامه‌ی ASP.NET Core

روش‌های متعددی برای گشودن کنسول خط فرمان در VS Code وجود دارند:
الف) فشردن دکمه‌های Ctrl+Shift+C
اینکار باعث می‌شود تا command prompt ویندوز از پوشه‌ی جاری به صورت مجزایی اجرا شود.
ب) فشردن دکمه‌های Ctrl+` (و یا Ctrl+ back-tick)
به این ترتیب کنسول پاورشل درون خود VS Code باز خواهد شد. اگر علاقمند نیستید تا از پاورشل استفاده کنید، می‌توانید این پیش‌فرض را به نحو ذیل بازنویسی کنید:
همانطور که در قسمت قبل نیز ذکر شد، از طریق منوی File->Preferences->Settings می‌توان به تنظیمات VS Code دسترسی یافت. پس از گشودن آن، یک سطر ذیل را به آن اضافه کنید:
 "terminal.integrated.shell.windows": "cmd.exe"
اکنون Ctrl+ back-tick را فشرده تا کنسول خط فرمان، داخل VS Code نمایان شود و سپس دستور ذیل را صادر کنید:
 D:\vs-code-examples\FirstAspNetCoreProject>dotnet run
Hosting environment: Production
Content root path: D:\vs-code-examples\FirstAspNetCoreProject
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.
در اینجا دستور dotnet run سبب کامپایل و همچنین اجرای پروژه شده‌است و اکنون این برنامه‌ی وب بر روی پورت 5000 قابل دسترسی است:



ساده سازی ساخت و اجرای یک برنامه‌ی ASP.NET Core در VS Code


زمانیکه گزینه‌ی افزودن امکانات ساخت و دیباگ را انتخاب می‌کنیم (تصویر فوق)، دو فایل جدید به پوشه‌ی vscode. اضافه می‌شوند:


دراینجا فایل tasks.json، حاوی وظیفه‌ای است جهت ساخت برنامه. یعنی بجای اینکه مدام بخواهیم به خط فرمان مراجعه کرده و دستوراتی را صادر کنیم، می‌توان از وظایفی که در پشت صحنه همین فرامین را اجرا می‌کنند، استفاده کنیم؛ که نمونه‌ای از آن، به صورت پیش فرض به پروژه اضافه شده است.
برای دسترسی به آن، دکمه‌های ctrl+shift+p را فشرده و سپس در منوی ظاهر شده، واژه‌ی build را جستجو کنید:


با انتخاب این گزینه (که توسط Ctrl+Shift+B هم در دسترس است)، کار ساخت برنامه انجام شده و dll مرتبط با آن در پوشه‌ی bin تشکیل می‌شود.
 
همچنین در اینجا برای ساخت و بلافاصله نمایش آن در مرورگر پیش فرض سیستم، می‌توان مجددا دکمه‌های ctrl+shift+p را فشرد و در منوی ظاهر شده، واژه‌ی without را جستجو کرد:


با انتخاب این گزینه (که توسط Ctrl+F5 نیز در دسترس است)، برنامه ساخته شده، اجرا و نمایش داده می‌شود و برای خاتمه‌ی آن می‌توانید دکمه‌های Ctrl+C را بفشارید تا کار وب سرور موقتی آن خاتمه یابد.


در قسمت بعد مباحث دیباگ برنامه و گردش کار متداول یک پروژه‌ی ASP.NET Core را بررسی خواهیم کرد.
مطالب
CheckBoxList در ASP.NET MVC

ASP.NET MVC به همراه HtmlHelper توکاری جهت نمایش یک ChekBoxList نیست؛ اما سیستم Model binder آن، این نوع کنترل‌ها را به خوبی پشتیبانی می‌کند. برای مثال، یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس یک کنترلر Home جدید را نیز به آن اضافه کنید. در ادامه، برای متد Index آن، یک View خالی را ایجاد نمائید. سپس محتوای این View را به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{
<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}

و کنترلر Home را نیز مطابق کدهای زیر ویرایش کنید:
using System.Web.Mvc;

namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
return View();
}

[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}

یک breakpoint را در تابع Index دوم که آرایه‌ای را دریافت می‌کند، قرار دهید. سپس برنامه را اجرا کرده، تعدادی از checkboxها را انتخاب و فرم نمایش داده شده را به سرور ارسال کنید:


بله. همانطور که ملاحظه می‌کنید، تمام عناصر ارسالی انتخاب شده که دارای نامی مشابه بوده‌اند، به یک آرایه قابل بایند هستند و سیستم model binder می‌داند که چگونه باید این اطلاعات را دریافت و پردازش کند.
از این مقدمه می‌توان به عنوان پایه و اساس نوشتن یک HtmlHelper سفارشی CheckBoxList استفاده کرد.
برای این منظور یک پوشه جدید را به نام app_code، به ریشه پروژه اضافه نمائید. سپس یک فایل خالی را به نام Helpers.cshtml نیز به آن اضافه کنید. محتوای این فایل را به نحو زیر تغییر دهید:

@helper CheckBoxList(string name, List<System.Web.Mvc.SelectListItem> items)
{
<div class="checkboxList">
@foreach (var item in items)
{
@item.Text
<input type="checkbox" name="@name"
value="@item.Value"
@if (item.Selected) { <text>checked="checked"</text> }
/>
< br />
}
</div>
}

و برای استفاده از آن، کنترلر Home را مطابق کدهای زیر ویرایش کنید:

using System.Collections.Generic;
using System.Web.Mvc;

namespace MvcApplication21.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
ViewBag.Tags = new List<SelectListItem>
{
new SelectListItem { Text = "Item1", Value = "Val1", Selected = false },
new SelectListItem { Text = "Item2", Value = "Val2", Selected = false },
new SelectListItem { Text = "Item3", Value = "Val3", Selected = true }
};
return View();
}

[HttpPost]
public ActionResult GetTags(string[] tags)
{
return View();
}

[HttpPost]
public ActionResult Index(string[] result)
{
return View();
}
}
}

و در این حالت View برنامه به شکل زیر درخواهد آمد:
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
@using (Html.BeginForm())
{

<input type='checkbox' name='Result' value='value1' />
<input type='checkbox' name='Result' value='value2' />
<input type='checkbox' name='Result' value='value3' />
<input type="submit" value="submit" />
}

@using (Html.BeginForm(actionName: "GetTags", controllerName: "Home"))
{
@Helpers.CheckBoxList("Tags", (List<SelectListItem>)ViewBag.Tags)
<input type="submit" value="submit" />
}

با توجه به اینکه کدهای Razor قرار گرفته در پوشه خاص app_code در ریشه سایت، به صورت خودکار در حین اجرای برنامه کامپایل می‌شوند، متد Helpers.CheckBoxList در تمام Viewهای برنامه در دسترس خواهد بود. در این متد، یک نام و لیستی از SelectListItemها دریافت می‌گردد. سپس به صورت خودکار یک CheckboxList را تولید خواهد کرد. برای دریافت مقادیر ارسالی آن به سرور هم باید مطابق متد GetTags تعریف شده در کنترلر Home عمل کرد. در اینجا Value عناصر انتخابی به صورت آرایه‌ای از رشته‌ها در دسترس خواهد بود.

روشی جامع‌تر
در آدرس زیر می‌توانید یک HtmlHelper بسیار جامع را جهت تولید CheckBoxList در ASP.NET MVC بیابید. در همان صفحه روش استفاده از آن، به همراه چندین مثال ارائه شده است:
https://github.com/devnoob/MVC3-Html.CheckBoxList-custom-extension