نظرات مطالب
رسم نمودار توسط Kendo Chart
امکان تهیه‌ی خروجی به صورت PDF، SVG و تصویر را دارد. کلمه‌ی export را در این صفحه جستجو کنید. یک مثال:
<div id="chart"></div>
<script>
  $("#chart").kendoChart({
      pdf: {
          fileName: "Products.pdf"
      },
      legend: {
          position: "bottom"
      },
      series: [
          { name: "Series 1", data: [1, 2, 3] },
          { name: "Series 2", data: [3, 4, 5] }
      ]
  });

  var chart = $("#chart").getKendoChart();
  chart.saveAsPDF();
</script>
اشتراک‌ها
ردیابی خطاهای JavaScript توسط Google Analytics
// Pure JavaScript errors handler
window.addEventListener('error', function (err) {
    var lineAndColumnInfo = err.colno ? ' line:' + err.lineno +', column:'+ err.colno : ' line:' + e.lineno;
    _gaq.push([
        '_trackEvent',
        'JavaScript Error',
        err.message,
        err.filename + lineAndColumnInfo + ' -> ' +  navigator.userAgent,
        0,
        true
    ]);
});
ردیابی خطاهای JavaScript توسط Google Analytics
نظرات مطالب
کار با Kendo UI DataSource
کدهای ASP.NET MVC مطلب «فعال سازی عملیات CRUD در Kendo UI Grid» را جهت دریافت پارامتر سفارشی به روز کردم.  
زمانیکه صفحه بندی فعال است، تمام پارامترها داخل یک کوئری استرینگ با فرمت جی‌سون قرار می‌گیرند. به این شکل:
{"param1":"val1","param2":"val2","take":10,"skip":0,"page":1,"pageSize":10,"sort":[{"field":"Id","dir":"desc"}]}
برای خواندن آن‌ها فقط کافی است یک کلاس سفارشی ایجاد کرد:
 // با ارث بری، خواص اضافی و سفارشی را به کلاس پایه اضافه می‌کنیم
public class CustomDataSourceRequest : DataSourceRequest
{
    public string Param1 { set; get; }
    public string Param2 { set; get; }
}
بعد بجای DataSourceRequest اصلی، از کلاس سفارشی حاوی پارامترهای اضافی استفاده خواهیم کرد:
 var request = JsonConvert.DeserializeObject<CustomDataSourceRequest>(queryString);
مطالب
توابع ارسال پیام و ارسال فایل به کانال‌های تلگرام
امروزه استفاده از کانال‌های تلگرام بسیار پر کاربرد شده و کاربران صفحات مجازی را مشغول کرده است. برنامه نویسان وب برای استفاده از API‌های تلگرام میتوانند به https://core.telegram.org/ مراجعه و توابع بیشتری را در پروژه‌های خود استفاده کنند.

  ایجاد یک کانال و بات
بعد از ایجاد یک کانال public نیاز است یک بات ایجاد کنید. سپس این بات را به عنوان Administrator به کانال خود اضافه کنید.


  ارسال پیام به کانال

var token="175287941:AAFcpXIIj1HuFlC5aB0QDBdWQBsTHqflkna"; // توکن خود را جایگزین کنید

var channelId ="@ChannelName";
var message ="تست ارسال";
 var uri = String.Format("https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}", Token, channelId, message);
using (WebClient client = new WebClient())
{
       dynamic s = client.DownloadString(uri);
}
بعد از ایجاد بات، سیستم به شما یک توکن می‌دهد که باید همان توکن را در ارسال‌ها استفاده کنید. دستورات بالا به این صورت عمل میکنند که از طرف توکن، به عنوان مدیر کانال، یک پیام در کانال درج می‌شود.


ارسال فایل به کانال

public bool SendFileToChannel(byte[] file, string fileName)
{
   var token="175287941:AAFcpXIIj1HuFlC5aB0QDBdWQBsTHqflkna"; // توکن خود را جایگزین کنید 
   var channelId ="@ChannelName"; 
           
   using (var client = new HttpClient())
   {
      var uri = String.Format("https://api.telegram.org/bot{0}/sendDocument?chat_id={1}", Token, channelId   );
       using (var multipartFormDataContent = new MultipartFormDataContent())
       {
            var streamContent = new StreamContent(new MemoryStream(file));
                        streamContent.Headers.Add("Content-Type", "application/octet-stream");
            streamContent.Headers.Add("Content-Disposition", "form-data; name=\"document\"; filename=\"" + fileName + "\"");
            multipartFormDataContent.Add(streamContent, "file", fileName);

            using (var message = client.PostAsync(uri, multipartFormDataContent))
            {
                   var contentString = message.Result.Content.ReadAsStringAsync();
            }
        }
     }
  
   return true;
}


طریقه فراخوانی تابع ارسال فایل
string path = @"c:\myFile.txt"; // All File Allowed : pdf, mp3, jpg ,...
string fileName = Path.GetFileName(path);
byte[] file = System.IO.File.ReadAllBytes(path);
SendFileToChannel(file, fileName);


ارسال پیام به یک کانال private
 برای ارسال پیام به یک کانال خصوصی ابتدا باید کانال عمومی داشته باشید. در تابع ارسال پیام به یک کانال تلگرام، متغیر uri هنگام اجرا دارای مقدار شده و شما میتوانید این آدرس را در مرورگر وارد کنید. اتفاقی که می‌افتد این است که ارسال به کانال انجام شده و نتیجه‌ی آن روی صفحه ظاهر میگردد.
 var uri = String.Format("https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}", Token, channelId, message);
در متن بازگشتی یک پارامتر بنام ChannelId نمایش داده می‌شود که معمولا بصورت یک عدد منفی مثل 1001001734568- است که این کد، کد یکتای کانال ایجاد شده می‌باشد. شما از این به بعد میتوانید کانال را Private یا حتی Rename کنید. همچنین میتوانید در ارسال به کانال، بجای استفاده از ChannelName@، از کد یکتای بدست آمده استفاده نمائید.
نظرات مطالب
ایجاد alert,confirm,prompt هایی متفاوت با jQuery Impromptu
سلام
خسته نباشید
من توی سایتم از ajax استفاده کردم و برای حذف یه رکورد اول از کاربر تاییدیه گرفتم . اگه کاربر روی دکمه‌ی بله کلیک کنه ، عملیات شروع میشه و رکورد حذف میشه .
آخر سر توی تابعی که نتیجه برمیگرده توش (handleServerResponse)، بر اساس نتیجه‌ی برگشتی پیغام مناسبی بایدنمایش داده بشه. اما هیچ پیغامی نشون داده نمیشه
خیلی چیزا رو آزمایش کردم و آخر سر به این نتیجه رسیدم که چون دو تا پیغام میخواد پشت سر هم نمایش داده بشه ، این مشکل پیش میاد :(
حتی وقتی که پیغام تایید حذف رو برداشتم ، دیدم که پیغام دوم میاد !
واااقعا نمیدونم علتش چیه ! ممنون میشم کمکم کنید.
اینم قسمتی از کدمه :
function deleteFile(location, filename)//حذف فیزیکی و منطقی فایل
{
    $.prompt("آیا برای حذف فایل موجود اطمینان دارید ؟", {
        title: '<img src="Images/warning.png" />',
        buttons: { "بله": true, "خیر": false },
        focus: 2,
        submit: function (e, v, m, f) {
            if (v == true) {
                var getdate = new Date();  //Used to prevent caching during ajax call
                if (xmlhttp) {
                    $("p#vtip").fadeOut("slow").remove(); //محو شدن tooltip
                    var id = document.getElementById("id").value;
                    var i = '3';
                    params = "id=" + id + "&i=" + i + "&filename=" + filename + "&location=" + location;
                    xmlhttp.open("POST", "FetchData/dbSearchDocument1.php", true);
                    xmlhttp.onreadystatechange = handleServerResponse4;
                    xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
                    xmlhttp.send(params);
                }
            }
            else {
            }
        }
    });
}
//-----------------------------------------------------------------------
function handleServerResponse4() {
    if ((xmlhttp.readyState == 1) || (xmlhttp.readyState == 2) || (xmlhttp.readyState == 3))//loading
    {
        document.getElementById("content").innerHTML = "<img src='Images/bigloading.gif'/>";
    }
    else if (xmlhttp.readyState == 4) //ready
    {
        if (xmlhttp.status == 200) {
            if (xmlhttp.responseText == 1) {
                $.prompt(". حذف فایل با موفقیت انجام شد ", {
                    title: '<img src="Images/warning3.png" />',
                    buttons: { "بستن": true }
                });
            }
            else {
                $.prompt("! حذف فایل با خطا مواجه شده است", {
                    title: '<img src="Images/warning2.png" />',
                    buttons: { "بستن": true }
                });
            }
        }
        else {
            alert("Error during AJAX call. Please try again");
        }
    }
}
//-----------------------------------------------------------------------
نظرات مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت دوم - پیاده سازی سمت کلاینت
پیاده سازی جستجوی بر روی این گرید، شامل موارد زیر است:
اضافه کردن دو خاصیت جدید به کلاس PagedQueryModel سمت کلاینت جهت مشخص سازی ستونی که قرار است بر روی آن جستجو انجام شود و همچنین مقدار آن:
export class PagedQueryModel {
  constructor(
    // ...
    public filterByColumn: string,
    public filterByValue: string,
  ) { }
}
سپس به ProductsListComponent دو متد زیر را اضافه می‌کنیم:
  doFilter() {
    this.queryModel.page = 1;
    this.getPagedProductsList();
  }

  resetFilter() {
    this.queryModel.page = 1;
    this.queryModel.filterByColumn = "";
    this.queryModel.filterByValue = "";
    this.getPagedProductsList();
  }
اولی کار جستجو را انجام می‌دهد و دومی بازگشت حالت گرید به وضعیت اول آن است. متد getPagedProductsList قابلیت واکشی خودکار اطلاعات دو خاصیت جدیدی را که اضافه کردیم دارد و نیازی به تنظیمات اضافه‌تری ندارد. یعنی filterByColumn و filterByValue را به صورت خودکار به سمت سرور ارسال می‌کند.

پس از آن، قالب این گرید (products-list.component.html) جهت افزودن جستجو، به صورت زیر تغییر می‌کند:
<div class="panel panel-default">
  <div class="panel-body">
    <div class="form-group">
      <input type="text" [(ngModel)]="queryModel.filterByValue" placeholder="Search For ..."
        class="form-control" />
    </div>
    <div class="form-group">
      <select class="form-control" name="filterColumn" [(ngModel)]="queryModel.filterByColumn">
        <option value="">Filter by ...</option>
        <option *ngFor="let column of columns" [value]="column.propertyName">
          {{ column.title }}
        </option>
      </select>
    </div>
    <button class="btn btn-primary" type="button" (click)="doFilter()">Search</button>
    <button class="btn btn-default" type="button" (click)="resetFilter()">Reset</button>
  </div>
</div>
که در آن queryModel.filterByColumn و queryModel.filterByValue از کاربر دریافت می‌شوند. همچنین دو متد doFilter و resetFilter را نیز فراخوانی می‌کند.
با این شکل:


تغییرات سمت سرور آن نیز به صورت ذیل است:
ابتدا IPagedQueryModel را با همان دو خاصیت جدید ستون فیلتر شونده و مقدار آن، تکمیل می‌کنیم:
    public interface IPagedQueryModel
    {
    // ....
        string FilterByColumn { get; set; }
        string FilterByValue { get; set; }
    }

    public class ProductQueryViewModel : IPagedQueryModel
    {
        // ... other properties ...

// ...
        public string FilterByColumn { get; set; }
        public string FilterByValue { get; set; }
    }
از این دو خاصیت جدید، جهت افزودن متد اعمال جستجو، همانند متد ApplyOrdering که پیشتر تعریف شد، استفاده می‌کنیم:
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyFiltering<T>(
          this IQueryable<T> query,
          IPagedQueryModel model,
          IDictionary<string, Expression<Func<T, object>>> columnsMap)
        {
            if (string.IsNullOrWhiteSpace(model.FilterByValue) || !columnsMap.ContainsKey(model.FilterByColumn))
            {
                return query;
            }

            var func = columnsMap[model.FilterByColumn].Compile();
            return query.Where(x => func(x).ToString() == model.FilterByValue);
        }
در اینجا همان columnsMap مورد استفاده در متد ApplyOrdering جهت نگاشت نام‌های رشته‌ای ستون‌ها به معادل Expression آن‌ها استفاده شده‌است.

در آخر، به کنترلر ProductController و اکشن متد GetPagedProducts آن مراجعه کرده و پیش از ApplyOrdering، متد جدید ApplyFiltering فوق را اضافه می‌کنیم:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyFiltering(queryModel, columnsMap);
query = query.ApplyOrdering(queryModel, columnsMap);

کدهای کامل این تغییرات را از اینجا می‌توانید دریافت کنید.
مطالب
هدایت درخواست فایل‌های استاتیک در ASP.NET MVC به یک کنترلر
فرض کنید یک پوشه Export در ریشه سایت دارید که حاوی تعدادی فایل PDF عمومی است.
سؤال: آیا می‌شود دسترسی به فایل‌های قرار گرفته در این پوشه عمومی را کنترل کرد؟ به نحوی که فقط کاربران عضو سایت پس از اعتبارسنجی بتوانند آن‌ها را دریافت کنند؟
پاسخ: شاید عنوان کنید که می‌توان از تگ location در فایل web.config برای اینکار استفاده کرد:
<location path="Export">
    <system.web>
      <authorization>
        <deny users="?" />
      </authorization>
    </system.web>
  </location>
این تنظیمات هیچ اثری بر روی فایل‌های استاتیک PDF ندارند؛ چون در IIS 6 از موتور ASP.NET رد نخواهند شد. مگر اینکه این نوع پسوند‌ها به aspnet_isapi.dll انتساب داده شوند. در IIS 7 به بعد این وضع بهبود یافته است. اگر تنظیم runAllManagedModulesForAllRequests در وب کانفیگ برنامه به true تنظیم شده باشد و برنامه در حالت Integrated pipeline بجای Classic mode اجرا شود، امکان مدیریت فایل‌های استاتیک نیز در برنامه‌های ASP.NET وجود دارد .

سؤال: آیا راه حلی وجود دارد که بتوان فایل‌های استاتیک را صرفنظر از نوع، نگارش و حالت اجرای IIS توسط موتور ASP.NET مدیریت کرد؟
پاسخ: بلی. در ASP.NET MVC با تنظیم یک سطر ذیل، اینکار انجام می‌شود:
public static void RegisterRoutes(RouteCollection routes)
{
   // ...
   routes.RouteExistingFiles = true;
   // ...
}
توضیحات:
RouteCollection در ASP.NET MVC به کمک امکانات MapPathBasedVirtualPathProvider خود، ابتدا درخواست رسیده را بررسی می‌کند. اگر این درخواست به یک فایل عمومی اشاره کند، کل سیستم مسیریابی را غیرفعال می‌کند. اما با تنظیم RouteExistingFiles دیگر این بررسی صورت نخواهد گرفت (به عبارتی در بالا بردن سرعت نمایشی سایت نیز تاثیر گذار خواهد بود؛ چون یکی از بررسی‌ها را حذف می‌کند).


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

نام پوشه قرار گرفته در ریشه سایت، Export است. بنابراین برای هدایت درخواست‌های رسیده به آن (پس از تنظیم فوق)، نیاز است یک کنترلر جدید را به نام Export نیز ایجاد کنیم:
using System.IO;
using System.Web.Mvc;

namespace Mvc4RouteExistingFiles.Controllers
{
    public class ExportController : Controller
    {
        public ActionResult Index(string id)
        {
            if (string.IsNullOrWhiteSpace(id))
            {
                return Redirect("/");
            }

            var fileName= Path.GetFileName(id);
            var path = Server.MapPath("~/export/"+ fileName);
            return File(path, System.Net.Mime.MediaTypeNames.Application.Octet, fileName);
        }
    }
}
البته بدیهی است در اینجا می‌توان فیلتر Authorize را به کل کنترلر اعمال کرد، یا هر تنظیم دیگری که نیاز است.
برای اینکه بررسی کنیم، آیا واقعا فایل‌های استاتیک قرار گرفته در پوشه Export به این کنترلر هدایت می‌شود یا خیر، یک breakpoint را بر روی سطر اول اکشن متد Index قرار می‌دهیم. برنامه را اجرا کنید ... کار نخواهد کرد، زیرا مسیر یک فایل فرضی به صورت ذیل:
 http://localhost/export/test.pdf
به اکشن متد Index کنترلر Export، نگاشت نخواهد شد (index در این مسیر ذکر نشده است).
برای حل این مشکل فقط کافی است مسیر یابی متناظری را تعریف کنیم:
using System.Web.Mvc;
using System.Web.Routing;

namespace Mvc4RouteExistingFiles
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.RouteExistingFiles = true;

            routes.MapRoute(
                name: "ExportRoute",
                url: "Export/{id}",
                defaults: new { controller = "Export", action = "Index", id = UrlParameter.Optional }
            );

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }
}
در اینجا ExportRoute را مشاهده می‌کنید که به آدرس‌هایی به فرم Export/id پاسخ می‌دهد. در این حالت به صورت خودکار با توجه به تنظیمات انجام شده، اکشن متدی که انتخاب می‌شود همان Index خواهد بود و نیازی به ذکر صریح آن نخواهد بود.
اینبار اگر برنامه را اجرا کنیم، breakpoint ما کار خواهد کرد:



تنظیمات ثانویه پس از فعال سازی RouteExistingFiles

در این حالت با فعال سازی مسیریابی فایل‌های موجود، دیگر هیچ فایل استاتیکی به صورت معمول در اختیار کاربران قرار نخواهد گرفت و اگر همانند توضیحات قبل برای آن‌ها کنترلر جداگانه‌ای را تهیه نکنیم، عملا سایت از کار خواهد افتاد.
برای رفع این مشکل، در ابتدای متد RegisterRoutes فوق، تنظیمات ذیل را اضافه کنید تا پوشه‌های content، scripts و همچنین یک سری فایل با پسوند مشخص، همانند سابق و مستقیما توسط سرور ارائه شوند؛ در غیراینصورت کاربر پیغام 404 را پس از درخواست آن‌ها، دریافت خواهد کرد:
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.IgnoreRoute("content/{*pathInfo}");
routes.IgnoreRoute("scripts/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");
routes.IgnoreRoute("{resource}.ico");
routes.IgnoreRoute("{resource}.png");
routes.IgnoreRoute("{resource}.jpg");
routes.IgnoreRoute("{resource}.gif");
routes.IgnoreRoute("{resource}.txt");
مطالب
استفاده از فایل Json برای ذخیره و بازیابی تنظیمات برنامه
قطعا شرایطی پیش خواهد آمد که شما مجبور شوید داده‌هایی را به عنوان تنظیمات برنامه در محلی ذخیره کنید و مجددا آن‌ها را فراخوانی کنید. روش‌های مختلفی برای این کار وجود دارند که معروف‌ترین و ساده‌ترین راه، استفاده از Settings خود پروژه می‌باشد. اما این به منزله بهترین راه نیست! در این مطلب قصد داریم تنظیمات برنامه را در یک فایل json، با همان ساختار استانداردش ذخیره و بازیابی کنیم.
برای اینکار نیاز به سریالایز و دیسریالایز کردن مدل داریم. اگر از دات نت کور استفاده میکنید، کتابخانه توکار جیسون، در فضای نام System.Text.Json از عهده این کار بر میاد و اگر از نسخه‌های دات نت فریمورک استفاده میکنید، باید پکیج newtonsoft.json را نصب کنید.
برای شروع یک کلاس را به نام GlobalData (یا هر نام دلخواه دیگری) ایجاد کنید.
چون قرار هست این کلاس هر نوع مدلی را برای ما سریالایز و دیسریالایز کند، پس کلاس را بصورت جنریک تعریف کنید.
public abstract class GlobalData<T> where T : GlobalData<T>, new()
حالا برای دسترسی به این کلاس، یک متغیر جنریک را به نام Config ایجاد میکنیم:
public static T Config { get; set; }
همینطور برای خواندن یک فایل جیسون از محلی مشخص، یک متغیر دیگر را به نام filename ایجاد میکنیم:
private static string _filename { get; set; }
در این کلاس به 2 متد نیاز داریم. متد اول برای دیسریالایز کردن فایل جیسون و متد دوم برای سریالایز کردن اطلاعات:
public static void Init(string FileName = "AppConfig.json")
        {
            _filename = FileName;
            if (File.Exists(FileName))
            {
                string json = File.ReadAllText(FileName);

                Config = (string.IsNullOrEmpty(json) ? new T() : JsonSerializer.Deserialize<T>(json)) ?? new T();
            }
            else
            {
                Config = new T();
            }
        }
بصورت پیشفرض محل خواندن فایل جیسون را در کنار فایل اجرایی exe و با نام AppConfig.json در نظر میگیریم. در صورتی که فایل ما موجود بود، به کمک ReadAllText محتوای فایل جیسون را میخوانیم و در صورتی که خالی نبود، اقدام به دیسریالایز کردن آن میکنیم.
در متد Save نیز:
public static void Save()
        {
            JsonSerializerOptions options = new JsonSerializerOptions { WriteIndented = true, IgnoreNullValues = true };

            string json = JsonSerializer.Serialize(Config, options);
            File.WriteAllText(_filename, json);
        }
پراپرتی Config را که شامل اطلاعات ما میباشد، در محل موردنظر سریالایز میکنیم.
حالا به سراغ پروژه‌ی دمو می‌رویم. یک کلاس را ایجاد کرده و از کلاس GlobalData ارث بری میکنیم:
internal class AppConfig : GlobalData<AppConfig>
حالا پراپرتی‌های دلخواه خود را ایجاد میکنیم:
 public string ServerUrl { get; set; } = "https://sub.deltaleech.com";
 public bool IsShowNotification { get; set; } = true;
 public NavigationViewPaneDisplayMode PaneDisplayMode { get; set; } = NavigationViewPaneDisplayMode.Left;

 public SkinType Skin { get; set; } = SkinType.Default;
دقت کنید که قبل از خواندن تنظیمات، باید ابتدا فایل تنظیمات را دیسریالایز کرده باشید. پس هنگام اجرای پروژه، متد Init را فراخوانی کنید:
protected override void OnStartup(StartupEventArgs e) {
GlobalData<AppConfig>.Init();
}
برای خواندن تنظیمات به این صورت عمل کنید:
var skin = GlobalData<AppConfig>.Config.Skin;
و برای ذخیره کردن :
GlobalData<AppConfig>.Config.Skin = Skin.Dark;
GlobalData<AppConfig>.Save();

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

مطالب
تبدیل زیرنویس‌های خاص پلورال‌سایت به فرمت SRT
یک سری از دوره‌های پلورال‌سایت دارای زیرنویس هستند که تحت عنوان Transcript در کنار آن‌ها قرار گرفته‌اند:


این زیرنویس‌ها فرمت ویژه‌ای دارند:
                <li class="transcript-module">
                    Introduction to ASP.NET MVC 4
                    <ul>
                            <li class="transcript-clip" data-p="author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building"><a href="javascript:void(0)" onclick="LaunchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building');">Introduction</a><br />
                                <div>
                                        <a href="javascript:void(0)" onclick="p(this);" data-s="1.636">Hi, this is Scott Allen and this is the first module in the course design</a>
                                </div>
                            </li>
                            <li class="transcript-clip" data-p="author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=1&amp;course=mvc4-building"><a href="javascript:void(0)" onclick="LaunchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=1&amp;course=mvc4-building');">Web Platform Installer</a><br />
                                <div>
                                ...
در آن، هر li که دارای کلاسی به نام transcript-clip است، حاوی یک div می‌باشد و این div دارای تعدادی لینک است. این لینک‌ها توسط ویژگی datas آن‌ها که بیانگر زمان شروع گفتگو است، مشخص می‌شوند و همینطور الی آخر. بنابراین اگر بخواهیم برای آن‌ها ساختاری را تهیه کنیم، به کلاس‌های ذیل خواهیم رسید:
    public class TranscriptClip
    {
        public string Title { set; get; }
        public IList<TranscriptItem> TranscriptItems { set; get; }
    }

    public class TranscriptItem
    {
        public double StartTime { set; get; }
        public string Text { set; get; }
    }
هر li دارای کلاس transcript-clip، یک شیء TranscriptClip را تشکیل می‌دهد. هر شیء TranscriptClip می‌تواند داری چندین TranscriptItem باشد.
برای استخراج این اطلاعات، یکی از بهترین ابزارها، کتابخانه HTML Agility pack است که توسط آن می‌توان به liهای یاد شده دسترسی یافت:
 var nodes = doc.DocumentNode.SelectNodes("//li[@class='transcript-clip']/div");
و سپس اطلاعات آن‌ها را استخراج نمود.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;

namespace PluralsightTranscripts
{
    public class TranscriptClip
    {
        public string Title { set; get; }
        public IList<TranscriptItem> TranscriptItems { set; get; }
    }

    public class TranscriptItem
    {
        public double StartTime { set; get; }
        public string Text { set; get; }
    }

    public class ExtractSubtitle
    {
        public static void ConvertToSrt(string fileName)
        {
            var transcriptClips = extractItems(fileName);
            var itemNumber = 1;
            foreach (var item in transcriptClips)
            {
                transcriptClipToSrt(item, itemNumber);
                itemNumber++;
            }
        }

        private static void transcriptClipToSrt(TranscriptClip item, int itemNumber)
        {                        
            var count = item.TranscriptItems.Count;
            var srtFileContent = transcriptItemsToSrt(item.TranscriptItems, count);
            var fileName = removeIllegalCharacters(string.Format("{0}-{1}.srt", itemNumber.ToString("00"), item.Title));            
            File.WriteAllText(fileName, srtFileContent);
        }

        private static string transcriptItemsToSrt(IList<TranscriptItem> items, int count)
        {
            var lineNumber = 1;
            var sb = new StringBuilder();
            for (int row = 0; row < count; row++)
            {
                sb.AppendLine(lineNumber.ToString(CultureInfo.InvariantCulture));
                sb.AppendLine(getTimeLine(items, count, row));
                sb.AppendLine(items[row].Text);
                sb.AppendLine(string.Empty);
                lineNumber++;
            }
            return sb.ToString();            
        }

        private static string getTimeLine(IList<TranscriptItem> items, int count, int row)
        {
            var startTs = TimeSpan.FromSeconds(items[row].StartTime);
            var endTs = row + 1 < count ? TimeSpan.FromSeconds(items[row + 1].StartTime) : TimeSpan.FromSeconds(items[row].StartTime + 5);
            return string.Format("{0} --> {1}", timeSpanToString(startTs), timeSpanToString(endTs));
        }

        private static string timeSpanToString(TimeSpan lineTs)
        {
            return string.Format("{0}:{1}:{2},{3}", lineTs.Hours.ToString("D2"), lineTs.Minutes.ToString("D2"), lineTs.Seconds.ToString("D2"), lineTs.Milliseconds.ToString("D3"));
        }

        private static string removeIllegalCharacters(string fileName)
        {
            string regexSearch = string.Format("{0}{1}",
                                               new string(Path.GetInvalidFileNameChars()),
                                               new string(Path.GetInvalidPathChars()));
            var r = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch)));
            return r.Replace(fileName, ".");
        }

        private static IList<TranscriptClip> extractItems(string fileName)
        {
            var htmlContent = File.ReadAllText(fileName);
            var results = new List<TranscriptClip>();

            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            var nodes = doc.DocumentNode.SelectNodes("//li[@class='transcript-clip']/div");

            foreach (var node in nodes)
            {
                var itemsList = new List<TranscriptItem>();
                var title = node.ParentNode.ChildNodes.First(x => x.Name == "a").InnerText;

                foreach (var childNode in node.ChildNodes)
                {
                    if (childNode.Name != "a") continue;

                    var dataS = childNode.Attributes.First(x => x.Name == "data-s");
                    itemsList.Add(new TranscriptItem
                    {
                        StartTime = double.Parse(dataS.Value),
                        Text = HttpUtility.HtmlDecode(childNode.InnerText.Trim())
                    });
                }

                results.Add(new TranscriptClip { TranscriptItems = itemsList, Title = title });
            }

            return results;
        }
    }
}
اگر این اطلاعات را کنار هم قرار دهیم، به کلاس کمکی فوق خواهیم رسید. کار با گره‌های li شروع می‌شود. سپس در این گره‌ها، کلیه گره‌های a یا لینک‌ها، یافت شده و سپس dataS و متن آن‌ها استخراج می‌شوند. اگر این‌ها را نهایتا کنار هم قرار دهیم، می‌توان به فرمت SRT متداول که اکثر پخش کننده‌های فایل‌های تصویری قادر به پردازش آن‌ها هستند، رسید.
فرمت SRT ساختار ساده‌ای دارد. هر گفتگوی آن حداقل از سه سطر تشکیل می‌شود. سطر اول یک شماره خود افزاینده است. سطر دوم زمان شروع و پایان گفتگو را مشخص می‌کند و سطر سوم بیانگر متن گفتگو است. برای مثال:
 1
00:00:01,636 --> 00:00:05,616
Hi, this is Scott Allen and this is the first module in the course design

دریافت پروژه کامل این مطلب
PluralsightTranscripts.zip
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
ساختار نهایی متود DownloadFile در ImageHandlerController :

[AllowAnonymous]
        [ResponseCache(VaryByHeader = "id;imgSize", Duration = 30, Location = ResponseCacheLocation.Client, NoStore = true)]
        public IActionResult DownloadFile([FromRoute]string id, [FromQuery] ImgSize imgSize)
        {
         //Tuple<string, string, string>(queryResult.FileName, queryResult.FileOnDs,queryResult.FileContentType)   
          var result = _baseFileService.GetFileNameAndFileNameOnDsAndFileType(id);
            if (result == null) return View("Error");

            var fileName = result.Item1;
            string userAgent = Request.Headers["User-Agent"];
            if (IsInternetExplorer(userAgent))
            {
                var htencode = HtmlEncoder.Create();
                var attachment = string.Format("attachment; filename=\"{0}\"", htencode.Encode(fileName));
                _httpContext.HttpContext.Response.Headers.Add("Content-Disposition", attachment);
            }
            var rootPath = Path.Combine(_hostingEnvironment.WebRootPath, _settingsAppPathConfig.Value.ServerImagesRootPath);
            var filepath = Path.Combine(rootPath, imgSize.ToString().ToLower(), result.Item2);
            var filefinalpath = "~/" + _settingsAppPathConfig.Value.ServerImagesRootPath + "/" + imgSize.ToString().ToLower() + "/" + result.Item2;
            if (!System.IO.File.Exists(filepath))
            {
                const string notFoundImage = "notFound.jpg";
                var notFoundpath = "~/" + _settingsAppPathConfig.Value.ServerImagesRootPath + "/"+ notFoundImage;
                string contentType;
                new FileExtensionContentTypeProvider().TryGetContentType(notFoundImage, out contentType);
                return File(notFoundpath, contentType, notFoundImage);
            }
            string contentTypebase;
            new FileExtensionContentTypeProvider().TryGetContentType(result.Item3, out contentTypebase);
            return File(filefinalpath, contentTypebase, fileName);
        }