مطالب
MVVM و نمایش دیالوگ‌ها

بسیاری از برنامه‌های دسکتاپ نیاز به نمایش پنجره‌های دیالوگ استاندارد ویندوز مانند OpenFileDialog و SaveFileDialog را دارند و سؤال اینجا است که چگونه اینگونه موارد را باید از طریق پیاده سازی صحیح الگوی MVVM مدیریت کرد؛ از آنجائیکه خیلی راحت در فایل ViewModel می‌توان نوشت new OpenFileDialog و الی آخر. این مورد هم یکی از دلایل اصلی استفاده از الگوی MVVM را زیر سؤال می‌برد : این ViewModel دیگر قابل تست نخواهد بود. همیشه شرایط آزمون‌های واحد را به این صورت در نظر بگیرید:
سروری وجود دارد در جایی که به آن دسترسی نداریم. روی این سرور با اتوماسیونی که راه انداخته‌ایم، آخر هر روز آزمون‌های واحد موجود به صورت خودکار انجام شده و یک گزارش تهیه می‌شود (مثلا یک نوع continuous integration سرور). بنابراین کسی دسترسی به سرور نخواهد داشت تا این OpenFileDialog ظاهر شده را مدیریت کرده، فایلی را انتخاب و به برنامه آزمون واحد معرفی کند. به صورت خلاصه ظاهر شدن هر نوع دیالوگی حین انجام آزمون‌های واحد «مسخره» است!
یکی از روش‌های حل این نوع مسایل، استفاده از dependency injection یا تزریق وابستگی‌ها است و در ادامه خواهیم دید که چگونه WPF‌ بدون نیاز به هیچ نوع فریم ورک تزریق وابستگی خارجی، از این مفهوم پشتیبانی می‌کند.

مروری مقدماتی بر تزریق وابستگی‌ها
امکان نوشتن آزمون واحد برای new OpenFileDialog وجود ندارد؟ اشکالی نداره، یک Interface بر اساس نیاز نهایی برنامه درست کنید (نیاز نهایی برنامه از این ماجرا فقط یک رشته LoadPath است و بس) سپس در ViewModel با این اینترفیس کار کنید؛ چون به این ترتیب امکان «تقلید» آن فراهم می‌شود.

یک مثال عملی:
ViewModel نیاز دارد تا مسیر فایلی را از کاربر بپرسد. این مساله را با کمک dependency injection در ادامه حل خواهیم کرد.
ابتدا سورس کامل این مثال:

ViewModel برنامه (تعریف شده در پوشه ViewModels برنامه):

namespace WpfFileDialogMvvm.ViewModels
{
public interface IFilePathContract
{
string GetFilePath();
}

public class MainWindowViewModel
{
IFilePathContract _filePathContract;
public MainWindowViewModel(IFilePathContract filePathContract)
{
_filePathContract = filePathContract;
}

//...

private void load()
{
string loadFilePath = _filePathContract.GetFilePath();
if (!string.IsNullOrWhiteSpace(loadFilePath))
{
// Do something
}
}
}
}

دو نمونه از پیاده سازی اینترفیس IFilePathContract تعریف شده (در پوشه Dialogs برنامه):

using Microsoft.Win32;
using WpfFileDialogMvvm.ViewModels;

namespace WpfFileDialogMvvm.Dialogs
{
public class OpenFileDialogProvider : IFilePathContract
{
public string GetFilePath()
{
var ofd = new OpenFileDialog
{
Filter = "XML files (*.xml)|*.xml"
};
string filePath = null;
bool? dialogResult = ofd.ShowDialog();
if (dialogResult.HasValue && dialogResult.Value)
{
filePath = ofd.FileName;
}
return filePath;
}
}

public class FakeOpenFileDialogProvider : IFilePathContract
{
public string GetFilePath()
{
return @"c:\path\data.xml";
}
}
}

و View برنامه:

<Window x:Class="WpfFileDialogMvvm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:WpfFileDialogMvvm.ViewModels"
xmlns:dialogs="clr-namespace:WpfFileDialogMvvm.Dialogs"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ObjectDataProvider x:Key="mainWindowViewModel"
ObjectType="{x:Type vm:MainWindowViewModel}">
<ObjectDataProvider.ConstructorParameters>
<dialogs:OpenFileDialogProvider/>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource mainWindowViewModel}}">

</Grid>
</Window>

توضیحات:
ما در ViewModel نیاز داریم تا مسیر نهایی فایل را دریافت کنیم و این عملیات نیاز به فراخوانی متد ShowDialog ایی را دارد که امکان نوشتن آزمون واحد خودکار را از ViewModel ما سلب خواهد کرد. بنابراین بر اساس نیاز برنامه یک اینترفیس عمومی به نام IFilePathContract را طراحی می‌کنیم. در حالت کلی کلاسی که این اینترفیس را پیاده سازی می‌کند، قرار است مسیری را برگرداند. اما به کمک استفاده از اینترفیس، به صورت ضمنی اعلام می‌کنیم که «برای ما مهم نیست که چگونه». می‌خواهد OpenFileDialogProvider ذکر شده باشد، یا نمونه تقلیدی مانند FakeOpenFileDialogProvider. از نمونه واقعی OpenFileDialogProvider در برنامه اصلی استفاده خواهیم کرد، از نمونه تقلیدی FakeOpenFileDialogProvider در آزمون واحد و نکته مهم هم اینجا است که ViewModel ما چون بر اساس اینترفیس IFilePathContract پیاده سازی شده، با هر دو DialogProvider یاد شده می‌تواند کار کند.
مرحله آخر نوبت به وهله سازی نمونه واقعی، در View برنامه است. یا می‌توان در Code behind مرتبط با View نوشت:

namespace WpfFileDialogMvvm
{
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainWindowViewModel(new OpenFileDialogProvider());
}
}
}

و یا از روش ObjectDataProvider توکار WPF هم می‌شود استفاده کرد؛ که مثال آن‌را در کدهای XAML مرتبط با View ذکر شده می‌توانید مشاهده کنید. ابتدا دو فضای نام vm و dialog تعریف شده (با توجه به اینکه مثلا در این مثال، دو پوشه ViewModels و Dialogs وجود دارند). سپس کار تزریق وابستگی‌ها به سازنده کلاس MainWindowViewModel،‌ از طریق ObjectDataProvider.ConstructorParameters انجام می‌شود:

<ObjectDataProvider x:Key="mainWindowViewModel" 
ObjectType="{x:Type vm:MainWindowViewModel}">
<ObjectDataProvider.ConstructorParameters>
<dialogs:OpenFileDialogProvider/>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>

نظرات مطالب
یافتن تداخلات Collations در SQL Server
تهیه اسکریپت تغییر Collation  تمامی  فیلدهای ناهمگون در  دیتابیس جاری :
DECLARE @defaultCollation NVARCHAR(1000)
SET @defaultCollation = CAST(
      DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS NVARCHAR(1000)
  )


select 'ALTER TABLE ' 
+ QUOTENAME(C.TABLE_SCHEMA)
+'.'+ QUOTENAME(C.Table_Name) 
+' ALTER COLUMN ' +' [' +Column_Name+'] ' 
+  DATA_TYPE+'('+cast(character_maximum_length as varchar(10))+')' +' COLLATE Persian_100_CI_AS ' 
+(case IS_NULLABLE when 'YES' then 'NULL' else 'NOT NULL' end )+';'

FROM   Information_Schema.Columns C
     INNER JOIN Information_Schema.Tables T
          ON  C.Table_Name = T.Table_Name
WHERE  T.Table_Type = 'Base Table'
     AND RTRIM(LTRIM(Collation_Name)) <> RTRIM(LTRIM(@defaultCollation))
 --AND DATA_TYPE='nvarchar'
 AND character_maximum_length>0
ORDER BY
     C.Table_Name,
     C.Column_Name

نظرات مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
سلام و ضمن تشکر؛ یک سوال داشتم که بیشتر در زمینه کارایی و بهینه سازی سیستم هست. در بخش "پیاده سازی فیلتر سفارشی JwtAuthorizeAttribute " همین مطلب یک قسمت از آن نوشتید "به ازای هر درخواست به سرور، دو بار بررسی بانک اطلاعاتی را خواهیم داشت"

برای اینکه رفت و برگشت برای هر درخواست به بانک اطلاعاتی در پروژه‌های بزرگ رو مدیریت بهینه کنیم چکار باید کرد؟ منظورم اینه وقتی تعداد کاربران زیاد باشه و متدهای زیادی هم در پروژه باشه که در هدر همه اونها باید این توکن و دسترسی‌ها چک بشه ممکنه سربار زیادی روی بانک اطلاعاتی داشته باشه. برای مدیریت بهتر این موارد من دو راه تست کرده بودم :
1. توی یک متغیر استاتیک اطلاعات توکن‌ها علاوه بر بانک اطلاعاتی ذخیره بشه و هردو با هم سینک باشند (موردی که خودتون هم اشاره فرموده بودید) بنابراین بیشترین درخواستها برای چک این مقادیر روی یک متغیر استاتیک هست که روی IIS فعال میشه و ممکنه رم زیادی البته بگیره و هر وقت هم IIS ریست شد دوباره اون لیست توکن‌های استاتیک از بانک اطلاعاتی فراخوانی میشه و برای درخواست‌های بعدی کاربران از اون متغیر استاتیک که لیست توکن‌ها هست رو چک می‌کنه.
2. راه دوم استفاده از بانک اطلاعاتی هست که رفت و برگشت به بانک اطلاعاتی برای هر درخواست رو زیاد می‌کنه و ممکنه سربار زیادی داشته باشه

البته من در پروژه از بانک اطلاعاتی مونگو استفاده کردم و این لیست توکن‌ها و کلیه بانک اطلاعاتی کاربران و غیره در اون ذخیره میشه .

سوال اینجاست که برای زمانی که کاربران زیاد و متدهای زیادی داریم که باید چک شوند راه حل بهینه برای انجام این مورد چه راهی هست؟
و اینکه آیا اگه از بانک اطلاعاتی Redis که بر روی رم قرار میگیره برای مدیریت توکن‌ها استفاده کنیم کارایی بهتر میشه یا باز هم همون مشکل قبلی رو داریم؟
مسیرراه‌ها
Entity framework code-first
شروع به کار با EF Code first

برای تکمیل بحث نیاز است تغییرات انجام شده از نگارش 4 به 6 را نیز مد نظر داشته باشید:


آشنایی با مباحث Migrations



آشنایی با تنظیمات نگاشت‌ها به دو روش استفاده از ویژگی‌ها و Fluent API



اعتبارسنجی و بررسی استثناءها



ردیابی تغییرات



استفاده از SQL خام و بانک‌های اطلاعاتی متفاوت

      نکات مهم کوئری نویسی در EF



      استفاده از EF در WPF


      لایه بندی پروژه‌های EF Code first



      پروژ‌ه‌های انجام شده با EF Code first

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

      آشنایی با انواع زیرنویس‌ها
      زیرنویس‌ها فرمت‌های مختلفی دارند مانند srt,sub idx,smi و ... ولی در حال حاضر معروف‌ترین و معتبرترین فرمت در بین همه‌ی فرمت‌ها Subrip  با پسوند SRT می‌باشد که قالب متنی به صورت زیر دارد:
      203
      00:16:38,731 --> 00:16:41,325
      <i>Happy Christmas, your arse
      I pray God it's our last</i>
      که باعث میشود حجم بسیار کمی در حد چند کیلوبایت داشته باشد.

      بررسی مشکل ما با زیرنویس در تلویزیون‌ها
      یکی از مشکلاتی که ما در اجرای زیرنویس‌ها بر روی تلویزیون‌ها داریم این است که حروف فارسی را به خوبی نمی‌شناسند و در هنگام نمایش با مشکل مواجه می‌شوند که البته در اکثر مواقع با تبدیل زیرنویس از ANSI به Unicode یا UTF-8 مشکل حل می‌شود. ولی در بعضی مواقع تلویزیون یا پلیرها از پشتیبانی زبان فارسی سرباز می‌زنند و زیرنویس را به شکل زیر نمایش می‌دهند.
      سلام = م ا ل س
      به این جهت ما از یک برنامه به اسم srttouni استفاده می‌کنیم که با استفاده یک روش جایگزینی و معکوس سازی، مشکل ما را حل می‌کند. ولی باز هم این برنامه مشکلاتی دارد و از آنجا که برنامه نویس این برنامه که واقعا کمال تشکر را از ایشان، دارم مشخص نیست، مجبور شدم به جای گزارش، خودم این مشکلات را حل کنم. 
      مشکلات این برنامه :
      • عدم حذف تگ‌ها ، گاها برنامه نویس‌ها از تگ هایی چون Bold,italic,underline,color استفاده می‌کنند که معدود برنامه‌هایی آن را پشتیبانی کرده و تلویزیون و پلیرها هم که اصلا پشتیبانی نمی‌کنند و باعث میشود که متن روی تلویزیون مثل کد html ظاهر شود
      • بعضی جملات دوبار روی صفحه ظاهر می‌شوند.
      • تنها یک فایل را در هر زمان تبدیل می‌کند. مثلا اگر یک سریال چند قسمته داشته باشید، برای هر قسمت باید زیرنویس را انتخاب کرده و تبدیل کنید، در صورتی که میتوان دستور داد تمام زیرنویس‌های داخل دایرکتوری را تبدیل کرد یا چند زیرنویس را برای این منظور انتخاب کرد.

      نحوه‌ی خواندن زیرنویس با کدنویسی
      با تشکر از دوست عزیز ما در این صفحه می‌توان گفت یک کد تقریبا خوب و جامعی را برای خواندن این قالب داریم. بار دیگر نگاهی به قالب یک دیالوگ در زیرنویس می‌اندازیم و آن را بررسی می‌کنیم:
      203
      00:16:38,731 --> 00:16:41,325
      <i>Happy Christmas, your arse
      I pray God it's our last</i>
      اولین خط شامل شماره‌ی خط است که از یک آغاز می‌گردد تا به تعداد دیالوگ‌ها، خط دوم، زمان آغاز و پایان دیالوگ مورد نظر است، موقعی که دیالوگ روی صفحه ظاهر میشود تا موقعی که دیالوگ از روی صفحه محو شود که به ترتیب بر اساس ساعت:دقیقه:ثانیه و میلی ثانیه می‌باشد. خطوط بعدی هم متن دیالوگ است است و بعد از پایان متن دیالوگ یک خط خالی زیر آن قرار می‌گیرد تا نشان دهد این دیالوگ به پایان رسیده است. اگر همین خط خالی حذف گردد برنامه‌هایی چون Media player classic خطهای زیری را جز متن دیالوگ قبلی به حساب می‌آورند و شماره خط و زمان بندی دیالوگ بعدی به عنوان متن روی صفحه ظاهر می‌گردند و بعضی player‌ها هم قاطی کرده و کلا زیرنویس را نمی‌خوانند یا اون خط رو نشون نمیدن مثل Kmplayer و هر کدام رفتار خاص خودشان را بروز می‌دهند.
      کد زیر در کلاس SubRipServices وظیفه‌ی خواندن محتوای فایل srt را بر اساس عبارتی که دادیم دارد:
      private readonly static Regex regex_srt = new Regex(@"(?<sequence>\d+)\r\n(?<start>\d{2}\:\d{2}\:\d{2},\d{3}) --\> " +
                  @"(?<end>\d{2}\:\d{2}\:\d{2},\d{3})\r\n(?<text>[\s\S]*?)\r\n\r\n", RegexOptions.Compiled);
      
       public string ToUnicode(string lines)
              {
      
              string subtitle= regex_srt.Replace(lines,delegate(Match m)
                   {
                       string text = m.Groups["text"].Value;
                       //1.remove tags
                       text = CleanScriptTags(text);
      
                       //2.replace letters
                       PersianReshape reshaper = new PersianReshape();
                       text = reshaper.reshape(text);
                       string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                       text = "";
                       foreach (string line in splitedlines)
                       {
                           //3.reverse tags
                           text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                       }
                       return
                           string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value,
                               m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
                   }
                  );
      
                  return subtitle;
              }
      در اولین خط ما یک Regular Expersion یا یک عبارت با قاعده تعریف کردیم که در اینجا میتوانید با خصوصیات آن آشنا شوید. ما برای این کلاس یک الگو ایجاد کردیم و بر حسب این الگو، متن یک زیرنویس را خواهد گشت و خطوطی را که با این تعریف جور در می‌آیند و معتبر هستند، برای ما باز می‌گرداند.
      عبارتهایی که به صورت <name>? تعریف شده‌اند در واقع یک نامگذاری برای هر قسمت از الگوی ما هستند تا بعدا این امکان برای ما فراهم شود که خطوط برگشتی را تجزیه کنیم که مثلا فقط قسمت متن را دریافت کنیم، یا فقط قسمت زمان شروع یا پایان را دریافت کنیم و ...
      متد tounicode یک آرگومان متنی دارد (lines) که شامل محتویات فایل  زیرنویس است. متد Replace در شی regex_srt با هر بار پیدا کردن یک متن بر اساس الگو در رشته lines دلیگیتی را فرا می‌خواند که در اولین پارامتر آن که از نوع matchEvaluator است، شامل اطلاعات متنی است که بر اساس الگو، یافت شده است. خروجی آن از نوع string می‌باشد که با متن پیدا شده بر اساس الگو جابجا خواهد کرد و در نهایت بعد از چندین بار اجرا شدن، کل متن‌های تعویض شده، به داخل متغیر subtitle ارسال خواهند شد.
      کاری که ما در اینجا می‌کنیم این است که هر دیالوگ داخل زیرنویس را بر اساس الگو، یافته و متن آن را تغییر داده و متن جدید را جایگزین متن قبلی می‌کنیم. اگر زیرنویس ما 800 دیالوگ داشته باشد این دلیگیت 800 مرتبه اجرا خواهد شد.
      از آنجا که ما تنها می‌خواهیم متن زیرنویس را تغییر دهیم، در اولین خط فرامین این دلیگیت تعریف شده، متن مورد نظر را بر اساس همان گروه‌هایی که تعریف کرده‌ایم دریافت می‌کنیم و در متغیر text قرار می‌دهیم:
      m.Groups["text"].Value
      در مرحله‌ی بعدی ما اولین مشکلمان (حذف تگ‌ها)  را با تابعی به اسم CleanScriptTags برطرف میکنیم که کد آن به شرح زیر است:
       private static readonly Regex regex_tags = new Regex("<.*?>", RegexOptions.Compiled);
       private  string CleanScriptTags(string html)
              {
                  return regex_tags.Replace(html, string.Empty);
              }
      کد بالا از یک regular Expression دیگر جهت پیدا کردن تگ‌ها استفاده می‌کند و به جای آن‌ها عبارت "" را جایگزین می‌کند. این کد قبلا در سایت جاری در این صفحه توضیح داده شده است. خروجی این تابع را مجددا در text قرار می‌دهیم و به مرحله‌ی دوم، یعنی تعویض کاراکترها می‌رویم:
       PersianReshape reshaper = new PersianReshape();
                       text = reshaper.reshape(text);
                       string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                       text = "";
                       foreach (string line in splitedlines)
                       {
                           //3.reverse tags
                           text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                       }
      برای اینکه دقیقا متوجه شویم قرار است چکاری انجام شود بیاید دو گروه یا بلوک مختلف در یونیکد را بررسی کنیم. هر بلوک کد در یونیکد شامل محدوده‌ای از کد پوینت هاست که نامی منحصرفرد برای خود دارد و هیچ کدام از کدپوینت‌ها در هر بلوک یا گروه، اشتراکی با بقیه‌ی بلوک‌ها ندارد. سایت codetable از آن دست سایت‌هایی است که اطلاعات خوبی در مورد کدهای یونیکد دارد. در قسمت Unicode Groups دو گروه برای زبان عربی وجود دارند که در جدول این گروه، هر سطر آن یکی از کدها را به صورت دسیمال، هگزا دسیمال و نام و نماد آن، نمایش می‌دهد.
      ^  ,   ^   Arabic Presentation Forms-A 
      ^^  Arabic Presentation Forms-B 
      بلوک اول طبق گفته‌ی ویکی پدیا دسته‌ی متنوعی از حروف مورد نیاز برای زبان فارسی، اردو، پاکستانی و تعدادی از زبان‌های آسیای مرکزی است.
      بلوک دوم شامل نمادها و نشانه‌های زبان عربی است و در حال حاضر برای کد کردن استفاده نمی‌شوند و دلیل حضور آن برای سازگاری با سیستم‌های قدیمی است.
      اگر خوب به مشکلی که در بالا برای زیرنویس‌ها اشاره کردیم دقت کنید، گفتیم حروف از هم جدا نشان داده می‌شوند و اگر به بلوک دوم در لینک‌های داده شده نگاه کنید می‌بینید که حروف متصل را داراست. یعنی برای حرف س 4 حرف یا کدپوینت داراست : سـ برای کلماتی مثل سبد، ـس برای کلماتی مثل شانس، ـسـ برای کلماتی مثل بسیار، ولی خود س برای کلمات غیر متصل مثل ناس، البته بعضی حروف یک یا دو حالت می‌طلبند مثل د، ر که فقط دو حالت ـد و د ، ـر و ر را دارند یا مثل آ که یک حالت دارد.
      من قبلا یک کلاس به نام lettersTable ایجاد کرده بودم (و دیگر نوشتن آن را ادامه ندادم) که برای هر حرف، یک آیتم در شی‌ءایی از نوع dictionary ساخته بودم و هر کدپوینت بلوک اول را در آن کلید و کد متقابلش را در بلوک دوم، به صورت مقدار ذخیره کرده بودم (گفتیم که هر نماد در بلوک اول، برابر با 4 نماد در بلوک دوم است؛ ولی ما در دیکشنری تنها مقدار اول را ذخیره می‌کنیم. زیرا کد بقیه نمادها دقیقا پشت سر یکدیگر قرار گرفته‌اند که می‌توان با یک جمع ساده از عدد 0 تا 3، به مقدار هر کدام از نمادها رسید. البته ناگفته نماند بعضی نمادها 2 عدد بودند که این هم باید بررسی شود). برای همین هر کاراکتر را با کاراکتر قبل و بعد می‌گرفتم و بررسی می‌کردم و از یک جدول دیکشنری دیگر هم به اسم specialchars هم استفاده کردم تا آن کاراکترهایی که تنها دو نماد یا یک نماد را دارند، بررسی کنم و این کاراکترها همان کاراکترهایی بودند که اگر قبل یک حرف هم بیایند، حرف بعدی به آن‌ها نمی‌چسبد. برای درک بهتر، این عبارت مثال زیر را  برای حرف س در نظر بگیرید:
      مستطیل = چون بین هر دو طرف س حر وجود دارد قطعا باید شکل س به صورت ـسـ انتخاب شود ، حالا مثال زیر را در نظر بگیرید:
      دست = دـست که اشتباه است و باید باشد دست یعنی شکل سـ باید صدا زده شود، پس این مورد هم باید لحاظ شود.
      نمونه‌ای از کد این کلاس:
      Dictionary<int ,int>  letters=new Dictionary<int, int>();
      
         //0=0x0 ,1=1x0 ,2=0x1 ,3=1x1
              private void FillPrimaryTable()
              {
                  //آ
                  letters.Add(1570, 65153);
                  //ا
                  letters.Add(1575, 65166);
                  //أ
                  letters.Add(1571, 65155);
                  //ب
                  letters.Add(1576, 65167);
                  //ت
                  letters.Add(1578, 65173);
                  //ث
                  letters.Add(1579, 65177);
                  //ج
                  letters.Add(1580, 65181);
      .....
      }
      
      Dictionary<int,byte> specialchars=new Dictionary<int, byte>();
      
        private void SetSpecialChars()
              {
                  //آ
                  specialchars.Add(1570, 0);
                  //ا
                  specialchars.Add(1575, 0);
                  //د2
                  specialchars.Add(1583, 1);
                  //ذ2
                  specialchars.Add(1584, 1);
                  //ر2
                  specialchars.Add(1585, 1);
                  //ز2
                  specialchars.Add(1586, 1);
                  //ژ
                  specialchars.Add(1688, 1);
                  //و2
                  specialchars.Add(1608, 1);
                  //أ
                  specialchars.Add(1571, 1);
      
              }
      کلاس بالا تنها برای ذخیره‌ی کدپوینت‌ها بود، ولی یک کلاس دیگر هم به اسم lettersCrawler نوشته بودم که متد آن وظیفه‌ی تبدیل را به عهده داشت.

      در آن متد هر بار یک حرف را انتخاب می‌کرد و حرف قبلی و بعدی آن را ارسال می‌کرد تا تابع CalculateIncrease آن را محاسبه کرده و کاراکتر نهایی را باز گرداند و به متغیر finalText اضافه می‌کرد. ولی در حین نوشتن، زمانی را به یاد آوردم که اندروید به تازگی آمده بود و هنوز در آن زمان از زبان فارسی پشتیبانی نمی‌کرد و حروف برنامه‌هایی که می‌نوشتیم به صورت جدا از هم بود و همین مشکل را داشت که ما این مشکل را با استفاده از یک کلاس جاوا که دوست عزیزی آن را در اینجا به اشتراک گذاشته بود، حل می‌کردیم. پس به این صورت بود که از ادامه‌ی نوشتن کلاس انصراف دادم و از یک کلاس دقیق‌تر و آماده استفاده کردم.
      در واقع این کلاس همین کار بالا را با روشی بهتر انجام می‌دهد. همه‌ی نمادها به طور دقیق‌تری کنترل می‌شوند حتی تنوین‌ها و دیگر علائم، همه نمادها با کدهای متناظر در یک آرایه ذخیره شده‌اند که ما در بالا از نوع Dictionary استفاده کرده بودیم.
      تنها کاری که نیاز بود، باید این کد به سی شارپ تبدیل میشد و از آنجایی که این دو زبان خیلی شبیه به هم هستند، حدود ده دقیقه‌ای برای ویرایش کد وقت برد که می‌توانید کلاس نهایی را از اینجا دریافت کنید.
      پس خط زیر در متد ToUnicode کار تبدیل اصلی را صورت می‌دهد:
        PersianReshape reshaper = new PersianReshape();
                       text = reshaper.reshape(text);
      بنابراین مرحله‌ی دوم انجام شد. این تبدیل در بسیاری از سیستم‌ها همانند اندروید کافی است؛ ولی ما گفتیم که تلویزیون یا پلیر به غیر از جدا جدا نشان دادن حروف، آن‌ها را معکوس هم نشان می‌دهند. پس باید در مرحله‌ی بعد آن‌ها را معکوس کنیم که اینکار با خط زیر و صدا زدن تابع ReverseText انجام میگیرد
       //3.reverse tags
                       text = ReverseText(text);
      از آنجا که یک دیالوگ ممکن است چند خطی باشد، این معکوس سازی برای ما دردسر می‌شد و ترتیب خطوط هم معکوس می‌شد. پس ما با استفاده از کد زیر هر یک خط را شکسته و هر کدام را جداگانه معکوس می‌کنیم و سپس به یکدیگر می‌چسبانیم:
      string[] splitedlines = text.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
                       text = "";
                       foreach (string line in splitedlines)
                       {
                           //3.reverse tags
                           text += ReverseText(reshaper.reshape(line))+Environment.NewLine ;
                       }
      همه‌ی ما معکوس سازی یک رشته را بلدیم، یکی از روش‌ها این است که رشته را خانه به خانه از آخر به اول با یک for بخوانیم یا اینکه رشته را به آرایه‌ای از کارکاکترها، تبدیل کنیم و سپس با Array.Reverse آن را معکوس کرده و خانه به خانه به سمت جلو بخوانیم و خیلی از روش‌های دیگر. ولی این معکوس سازی‌ها برای ما یک عیب هم دارد و این هست که این معکوس سازی روی نمادهایی چون . یا ! و  غیره که در ابتدا و انتهای رشته آمده‌اند و حروف انگلیسی، نباید اتفاق بیفتند. پس می‌بینیم که تابع معکوس سازی هم باز باید ویژه‌تر باشد. ابتدا قسمت‌های ابتدا و انتها را جدا کرده و از آن حذف می‌کنیم. سپس رشته را معکوس می‌کنیم. ولی ممکن هست و احتمال دارد که بین حروف فارسی هم حروف انگلیسی یا اعداد به کار رود که آن‌ها هم معکوس می‌شوند. برای همین بعد از معکوس سازی یکبار هم باید آن‌ها را با یک عبارت با قاعده یافته و سپس هر کدام را جداگانه معکوس کرده و سپس مثل روش بالا Replace کنیم و رشته‌های جدا شده را به ابتدا و انتهای آن، سر جای قبلیشان می‌چسبانیم.
      این دو تابع برای معکوس کردن عادی یک رشته به کار می‌روند:
          private string Reverse(string text)
              {
                  return Reverse(text,0,text.Length);
              }
      
              private string Reverse(string text,int start,int end)
              {
                  if (end < start)
                      return text;
                  string reverseText = "";
      
                  for (int i = end-1; i >=start; i--)
                  {
                      reverseText += text[i];
                  }
                  return reverseText;
              }
      ولی این تابع ReverseText جمعی از عملیات معکوس سازی ویژه‌ی ماست؛ مرحله اول، مرحله دریافت و ذخیره‌ی حروف خاص در ابتدای رشته به اسم پیشوند prefix است:
        private string ReverseText(string text)
              {
                  char[] chararray = text.ToCharArray();
                  string reverseText = "";
                  bool prefixcomp = false;
                  bool postfixcomp = false;
                  string prefix = "";
                  string postfix = "";
      
                  #region get prefix symbols
                  for (int i = 0; i < chararray.Length; i++)
                  {
                      if (!prefixcomp)
                      {
                          char ch =(char) chararray.GetValue(i) ;
                          if (ch< 130)
                          {
                              prefix += chararray.GetValue(i);
                          }
                          else
                          {
                              prefixcomp = true;
                              break;
                          }
                      }
                  }
                  #endregion
      }
      مرحله‌ی دوم هم دریافت و ذخیره‌ی حروف خاص در انتهای رشته به اسم پسوند postfix است که به این تابع اضافه می‌کنیم:
       #region get postfix symbols
                  for (int i = chararray.Length - 1; i >-1 ; i--)
                  {
                      if (!postfixcomp && prefix.Length!=text.Length)
                      {
                          char ch = (char)chararray.GetValue(i);
                          if (ch < 130)
                          {
                              postfix += chararray.GetValue(i);
                          }
                          else
                          {
                              postfixcomp = true;
                              break;
                          }
                      }
                  }
                  #endregion
      مرحله‌ی سوم عملیات معکوس سازی روی رشته است و سپس با استفاده از یک Regular Expression حروف انگلیسی و اعداد بین حروف فارسی را یافته و یک معکوس سازی هم روی آن‌ها انجام می‌دهیم تا به حالت اولشان برگردند. کل عملیات معکوس سازی در اینجا به پایان می‌رسد:
        #region reverse text
      
                  reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length);
      
              
                  reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m)
                  {
                      return Reverse(m.Value);
                  });
                  #endregion
      تعریف عبارت با قاعده‌ی بالا به اسم unTargetedLetters:
      private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
      آخر سر هم رشته را به‌علاوه پیشوند و پسوند جدا شده بر می‌گردانیم:
      return prefix+ reverseText+postfix;
      کد کامل تابع بدین شکل در می‌آید:
      private static readonly Regex unTagetdLettersRegex = new Regex(@"[A-Za-z0-9]+", RegexOptions.Compiled);
      private string ReverseText(string text)
              {
                  char[] chararray = text.ToCharArray();
                  string reverseText = "";
                  bool prefixcomp = false;
                  bool postfixcomp = false;
                  string prefix = "";
                  string postfix = "";
      
                  #region get prefix symbols
                  for (int i = 0; i < chararray.Length; i++)
                  {
                      if (!prefixcomp)
                      {
                          char ch =(char) chararray.GetValue(i) ;
                          if (ch< 130)
                          {
                              prefix += chararray.GetValue(i);
                          }
                          else
                          {
                              prefixcomp = true;
                              break;
                          }
                      }
                  }
                  #endregion
      
                  #region get postfix symbols
                  for (int i = chararray.Length - 1; i >-1 ; i--)
                  {
                      if (!postfixcomp && prefix.Length!=text.Length)
                      {
                          char ch = (char)chararray.GetValue(i);
                          if (ch < 130)
                          {
                              postfix += chararray.GetValue(i);
                          }
                          else
                          {
                              postfixcomp = true;
                              break;
                          }
                      }
                  }
                  #endregion
      
                  #region reverse text
      
                  reverseText = Reverse(text, prefix.Length, text.Length-postfix.Length);
      
              
                  reverseText = unTagetdLettersRegex.Replace(reverseText, delegate(Match m)
                  {
                      return Reverse(m.Value);
                  });
                  #endregion
      
                
      
                  return prefix+ reverseText+postfix;
              }
      در نهایت، خط آخر دلیگت همه چیز را طبق فرمت یک دیالوگ srt چینش کرده و بر می‌گردانیم.
      return
                           string.Format("{0}\r\n{1} --> {2}\r\n", m.Groups["sequence"], m.Groups["start"].Value,
                               m.Groups["end"]) + text + Environment.NewLine+Environment.NewLine ;
      رشته subtitle را به صورت srt ذخیره کرده و انکودینگ را هم Unicode انتخاب کنید و تمام.

      نمایی از برنامه‌ی نهایی


      اجرای زیرنویس تبدیل شده روی کامپیوتر


      روی پلیر یا تلویزیون



        نکته‌ی نهایی: هنگام تست زیرنویس روی فیلم متوجه شدم پلیر خطوط بلند را که در صفحه‌ی نمایش جا نمی‌شود، می‌شکند و به دو خط تقسیم می‌کند. ولی نکته‌ی خنده دار اینجا بود که خط اول را پایین می‌اندازد و خط دوم را بالا. برای همین این تکه کد را نوشتم و به طور جداگانه در گیت هاب هم قرار داده‌ام.
       
      این تکه کد را هم بعد از
      //1.remove tags
                       text = CleanScriptTags(text);
       به برنامه اضافه می‌کنیم:
        text =StringUtils.ConvertToMultiLine(text);
      از این پس خطوط به طولی بین 30 کاراکتر تا چهل کاراکتر  شکسته خواهند شد و مشکل خطوط بلند هم نخواهیم داشت.
      کد متد ConvertToMultiline:
      namespace Utils
      {
          public static class StringUtils
          {
              public static string ConvertToMultiLine(String text, int min = 30, int max = 40)
              {
                  if (text.Trim() == "")
                      return text;
      
                  string[] words = text.Split(new string[] { " " }, StringSplitOptions.None);
      
                  string text1 = "";
                  string text2 = "";
                  foreach (string w in words)
                  {
                      if (text1.Length < min)
                      {
                          if (text1.Length == 0)
                          {
                              text1 = w;
                              continue;
                          }
      
                          if (w.Length + text1.Length <= max)
                              text1 += " " + w;
                      }
                      else
                          text2 += w + " ";
      
                  }
                  text1 = text1.Trim();
                  text2 = text2.Trim();
                  if (text2.Length > 0)
                  {
                      text1 += Environment.NewLine + ConvertToMultiLine(text2, min, max);
                  }
                  return text1;
              }
            
          }
      }
      آرگومان‌های min و max که به طور پیش فرض 30 و 40 هستند، سعی می‌کنند که هر خط را در نهایت به طور حدودی بین 30 تا 40 کاراکتر نگه دارند.
      نکته پایانی : خوشحال میشم دوستان در این پروژه مشارکت داشته باشند و اگر جایی نیاز به اصلاح، بهبود یا ایجاد امکانی جدید دارد  کمک حال باشند و سعی کنند تا آنجا که می‌شود برنامه را روی net frame work 2. نگه دارند و بالاتر نبرند. چون استفاده کننده‌های این برنامه کاربران عادی و گاها با دانش پایین هستند و خیلی از آن‌ها هنوز از ویندوز xp استفاده می‌کنند تا در اجرای برنامه خیلی دچار مشکل نشده و راحت برای بسیاری از آن‌ها اجرا شود.

      برنامه مورد نظر را به طور کامل می‌توانید از اینجا  یا اینجا به صورت فایل نهایی و هم سورس دریافت کنید. 
      نظرات مطالب
      سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
      در ساختار درونی سیستم ASP.NET Core Identity، در عمل چیزی به نام Role وجود خارجی ندارد. این Roleهای ظاهری فقط برای سازگاری با سیستم‌های membership خیلی قدیمی وجود دارند. تمام سیستم Identity بر اساس User Claims کار می‌کند. تمام Roleها و غیره در پشت صحنه ابتدا تبدیل به user claims می‌شوند و سپس استفاده خواهند شد. اطلاعات بیشتر: «ASP.NET Core Identity چگونه اطلاعات جدول AppUserClaims را پردازش می‌کند؟»
      مطالب
      شروع به کار با DNTFrameworkCore - قسمت 5 - مکانیزم Eventing و استفاده از سرویس‌های موجودیت‌ها
      در قسمت‌های قبل سعی شد یک دید کلی از نحوه استفاده از این زیرساخت ارائه شود؛ در این قسمت علاوه بر بررسی مکانیزم Eventing، با جزئیات بیشتری به استفاده از سرویس‌های پیاده‌سازی شده پرداخته خواهد شد.
      ‌‌‌‌‌

      مکانیزم Eventing

      ‌‌
      استفاده از رخ‌دادها، یکی از راه‌حل‌های رسیدن به  طراحی با Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) می‌باشد؛ همچنین برای حذف چرخه در فرآیند وابستگی مولفه‌های سیستم نیز مورد استفاده قرار میگیرد. در این زیرساخت برای Application Layer مبتنی‌بر CRUD، مکانیزم BusinessEvent با هدف در معرض دید قراردادن یکسری نقاط قابل گسترش توسط سایر بخش‌های سیستم، تعبیه شده است. برای استفاده از این مکانیزم لازم است بسته نیوگت زیر را نصب کنید:
      PM> Install-Package DNTFrameworkCore
      ‌‌‌
      سپس امکان این را خواهید داشت که مشترک رخ‌دادهای مرتبط با عملیات CUD متناظر با موجودیت‌های سیستم، شوید. به عنوان مثال، برای اینکه بتوان مشترک رخ‌داد ویرایش مرتبط با موجودیت Task شد، باید به شکل زیر عمل کرد:
      public class TaskEditingBusinessEventHandler : BusinessEventHandler<EditingBusinessEvent<TaskModel, int>>
      {
          private readonly ILogger<TaskEditingBusinessEventHandler> _logger;
      
          public TaskEditingBusinessEventHandler(ILogger<TaskEditingBusinessEventHandler> logger)
          {
              _logger = logger ?? throw new ArgumentNullException(nameof(logger));
          }
      
          public override Task<Result> Handle(EditingBusinessEvent<TaskModel, int> @event)
          {
              foreach (var model in @event.Models)
              {
                  _logger.LogInformation($"Title changed from: {model.OriginalValue.Title} to: {model.NewValue.Title}");
              }
      
              return Task.FromResult(Ok());
          }
      }

      کار با پیاده‌سازی واسط جنریک IBusinessEventHandler یا ارث‌بری از کلاس جنریک BusinessEventHandler آغاز می‌شود؛ سپس نیاز است Type Parameter متناظر را نیز مشخص کنیم. برای این منظور در تکه کد بالا از رخ‌داد جنریک EditingBusinessEvent استفاده شده است. همچنین همانطور که ملاحظه می‌کنید، نیاز است نوع Model مورد نظر نیز مشخص شده باشد؛ در اینجا از TaskModel به عنوان Model/DTO عملیات CUD موجودیت Task استفاده شده است.

      ‌‌‌‌رخ‌دادهای Creating/Created/Deleting/Deleted دارای خصوصیتی بنام Models هستند که نوع آن ‎IEnumerable<TModel>‎ می‌باشد. ولی این خصوصیت در رخ‌دادهای Editing/Edited از نوع ‎IEnumerable<ModifiedModel<TModel>> ‎ می‌باشد؛ در این صورت به مقادیر موجود در بانک اطلاعاتی و همچنین مقادیری که توسط استفاده کننده از سرویس جاری به عنوان آرگومان به متد ویرایش ارسال شده است، دسترسی خواهیم داشت.

      public class ModifiedModel<TValue>
      {
          public TValue NewValue { get; set; }
          public TValue OriginalValue { get; set; }
      }
      ‌‌‌‎‌‌‌‎‌
      نکته: همانطور که در قسمت‌های قبل اشاره شد، Application Layer مدنظر ما با یک Model/DTO برای عملیات CUD کار می‌کند؛ از این جهت، منطق تجاری و همچنین قواعد تجاری برفراز همان Model/DTO اجرا خواهند شد و به‌تبع آن، اگر سایر بخش‌های سیستم نیز قصد گسترش منطق تجاری مرتبط با یک موجودیت را دارند، باید با همان Model/DTO کار کنند.
      ‎‎‌‌
      چه زمانی استفاده از مکانیزم BusinessEvent مطرح شده توصیه می‌شود؟
      ‌‎‎‌‎
      به طور کلی محدودیتی در استفاده از آن وجود ندارد؛ در مواردی مشابه اگر قصد اعمال یکسری قواعد تجاری توسط سایر مولفه‌های سیستم را دارید و قصد ندارید ارجاعی به آن مولفه در مولفه جاری وجود داشته باشد یا بدلیل ایجاد چرخه، این امکان وجود ندارد، می‌توان از این مکانیزم بهره برد. برای مثال زمانی که یکسری قواعد تجاری جدید قرار است از سمت مولفه فروش بر روی مولفه مرتبط با مدیریت محصولات اعمال شود. 
      ‌‌‌
      نکته: اگر قصد ارائه یک رخ‌داد سفارشی را دارید، می‌توانید واسط IEventBus را تزریق کرده و از متد TriggerAsync آن استفاده کنید.
      ‌‌‌‌

      استفاده از سرویس‌های موجودیت‌ها

      ‌‌
      OOP : Everything is an object
      CRUD-based thinking : Everything is CRUD

      استفاده از سرویس‌های موجودیت‌ها به تولید CrudController مرتبط ختم نمی‌شود و در تفکر مبتنی‌بر CRUD، تمام عملیات مرتبط با یک موجودیت از یک تونل واحد عبور خواهند کرد. مسئول این تونل در ابتدا متد Create می‌باشد و در ادامه توسط متد Edit مدیریت می‌شود. به عنوان مثال، اگر امروز در یک سیستم رستورانی با نحوه‌های فروش مختلف، قرار باشد در زمان ثبت گروه کالایی جدید و براساس تنظیمات سیستم، آن گروه کالایی به صورت خودکار به لیست گروه‌های کالایی مرتبط با تمام نحوه‌های فروش اضافه شود، این امر باید از طریق منطق تجاری توسعه داده شده برای نحوه ‌فروش، انجام پذیرد. قرار نیست شما منطق تجاری مرتبط با نحوه فروش را دور بزنید و به صورت دستی شروع به ثبت این اطلاعات در بانک اطلاعاتی کنید. در این شرایط می‌بایست با استفاده از مکانیزم BusinessEvent به شکل زیر عمل کرد:

      public class ItemCategoryCreatedBusinessEventHandler : IBusinessEventHandler<CreatedBusinessEvent<ItemCategoryModel, int>>
      {
          private readonly ISaleMethodService _saleMethodService;
      
          public TaskEditingBusinessEventHandler(ISaleMethodService saleMethodService)
          {
              _saleMethodService = saleMethodService ?? throw new ArgumentNullException(nameof(saleMethodService));
          }
      
          public override Task<Result> Handle(CreatedBusinessEvent<ItemCategoryModel, int> @event)
          {
              var methods = _saleMethodService.FindAsnc();
      
              foreach (var method in methods)
              {
                  foreach (var model in @event.Models)
                  {
                      method.ItemCategories.Add(new SaleMethodItemCategoryModel
                      {
                          ItemCategoryId = model.Id,
                          TrackingState = TrackingState.Added;
                  });
              }
          }
      
           return _saleMethodService.EditAsync(methods);
      }
      ‌‌‌‌‌
      این آبونه شدن به رخ‌داد Created مرتبط با گروه کالایی، از سمت مولفه فروش انجام گرفته است. در بدنه متد Handle، ابتدا لیست نحوه‌های فروش موجود در سیستم توسط متد FindAsync بدون پارامتر واکشی شده و سپس با پیمایش خصوصیت Models مرتبط با رخ‌داد مدنظر، به‌ازای تک‌تک گروه‌های کالایی ثبت شده، یک وهله از SaleMethodItemCategoryModel به عنوان Detail موجودیت SaleMethod اضافه می‌شود. سپس با استفاده از متد EditAsync لیست این نحوه‌های فروش را ویرایش خواهیم کرد.
       
      نکته: در حد امکان این هندلرها را به صورت تک مسئولیتی طراحی کرده و توسعه دهید؛ این قضیه برای نوشتن آزمون‌های واحد مرتبط با هندلرها، حیاتی می‌باشد.

      نکته مهم: در مطلب «معرفی قالب پروژه Web API مبتنی‌بر ASP.NET Core Web API و زیرساخت DNTFrameworkCore» در رابطه با موضوع آزمون جامعیت سرویس‌ها بحث شد؛ توجه داشته باشید که اگر این هندلرها در فرآیند آزمون واحد سرویس‌ها وارد شوند، نگهداری داده‌های تست به‌شدت سخت و طاقت‌فرسا خواهد بود. راهکار پیشنهادی، استفاده از یک StubEventBust و جایگزینی آن با پیاده‌ساز پیش‌فرض، می‌باشد. از این طریق، فراخوانی هندلرهای مرتبط با رخ‌دادها را از فرآیند اصلی متدها حذف کرده‌ایم.
      مطالب
      ارتقاء پروژه‌های MSTest به نگارش 2 آن
      زمانیکه در ویژوال استودیو 2015، یک Unit Test Project جدید را ایجاد می‌کنید:


      پروژه‌ای را مبتنی بر نگارش قدیمی فریم ورک آزمون‌های واحد مایکروسافت و یا همان MSTest، ایجاد می‌کند. در ادامه روش ارتقاء این نوع پروژه‌ها را به نگارش 2 آن بررسی خواهیم کرد.


      پیشنیازهای کار با MSTest 2.x

      فریم ورک MSTest برای پشتیبانی از دات نت فریم ورک کامل و همچنین NET Core. ارتقاء یافته‌است و اینبار به صورت بسته‌های نیوگت ارائه می‌شود. بنابراین پس از ایجاد این نوع پروژه‌ها در یک پروژه‌ی از نوع دات نت فریم ورک کامل (NET 4.x.)، ابتدا نیاز است به ارجاعات پروژه مراجعه و سپس ارجاع به اسمبلی Microsoft.VisualStudio.QualityTools.UnitTestFramework را به صورت دستی حذف کرد.
      پس از آن باید از طریق نیوگت، دو بسته‌ی جدید ذیل را نصب کرد:
      PM> install-package MSTest.TestFramework -Pre
      PM> install-package MSTest.TestAdapter -Pre
      در اینجا ذکر سوئیچ pre، برای دریافت آخرین نگارش آن، الزامی است.


      پشتیبانی از DataRow در MSTest 2.x

      سایر فریم ورک‌های آزمون واحد، این امکان را فراهم می‌کنند تا بتوان بجای نوشتن چندین متد آزمون واحد برای بررسی پارامترهای مختلف ارسال شده‌ی به یک متد خاص، از طریق ویژگی‌ها بتوان این پارامترها را اعمال کرد. این قابلیت در MSTest 2.x با پشتیبانی از ویژگی جدید DataRow اضافه شده‌است:
      [TestClass]
      public class TestFrameworkTest
      {
          [TestMethod]
          public void SimpleTest()
          {
              Assert.IsTrue(false);
          }
      
          [DataTestMethod]
          [DataRow(1, 2, 3)]
          [DataRow(2, 2, 4)]
          [DataRow(3, 2, 6)]
          [DataRow(5, 2, 7)]
          public void RowTest(int a, int b, int result)
          {
              Assert.AreEqual(result, a + b);
          }
      }
      - حالت پیش فرض آزمون‌های واحد MSTest را در متد SimpleTest آن مشاهده می‌کنید. متدی که دارای پارامتر نیست و با ویژگی TestMethod مزین شده‌است.
      - در متد RowTest نوشته شده، نحوه‌ی بکارگیری ویژگی جدید DataRow را ملاحظه می‌کنید. در اینجا عملیات مدنظر، جمع زدن دو مقدار است. بجای اینکه 4 متد مختلف را برای بررسی اینکار تهیه کنیم، در اینجا می‌توان پارامترهای مورد نیاز را از طریق DataRow به متد RowTest ارسال کرد.


      نحوه‌ی اجرای آزمون‌های MSTest

      هرچند ReSharper قابلیت اجرای آزمون‌های MSTest را دارد، اما تا نگارش 2016.3 آن، از ویژگی جدید DataRow پشتیبانی نکرده و قادر به شناسایی آن‌ها نیست. بنابراین تنها روش اجرای این نوع آزمون‌ها، استفاده از همان روش استاندارد توکار ویژوال استودیو است و بسته‌ی MSTest.TestAdapter اضافه شده، آن‌را به روز رسانی می‌کند:
       Test Menu -> Windows -> Test Explorer


      همانطور که ملاحظه می‌کنید، به ازای هربار قید ویژگی DataRow، یکبار آزمون واحد را به صورت جداگانه تکرار کرده‌است.
      نظرات مطالب
      امکان استفاده‌ی مستقیم از کتابخانه‌های Full .NET Framework در NET Core 2.0.
      در شاخه پروژه net core 2 (پروژه وب asp.net core) یک پوشه به نام DLL وجود دارد و LC.dll خارجی (تولید شده با Full .Net FrameWork) را در آن قرار دادم. dll را به پروژه اضافه (add reference) می‌کنم.
      <ItemGroup>
          <Reference Include="LC">
            <HintPath>DLL\LC.dll</HintPath>
          </Reference>
      </ItemGroup>
       در زمان بیلد مشکلی وجود ندارد ولی در زمان اجرا با خطای پیدا نکردن dll مواجه می‌شوم.
      System.IO.FileNotFoundException: 'Could not load file or assembly 'LC, Version=1.0.0.0, Culture=neutral, .PublicKeyToken=null'. The system cannot find the file specified.'
      برای امتحان dll هایی با .net standard  هم تولید کردم و به صورت بالا به پروژه اضافه کردم ولی باز هم همین خطا را دریافت کردم. این در صورتی است که اگر در هر دو حالت بالا (Standard , Full .Net FrameWork) به پروژه اصلی، reference را به صورت پروژه‌ای (ProjectReference) اضافه کنیم هیچ مشکلی وجود ندارد.

      بروزرسانی (حل مشکل):

      باید Copy to Output Drirectory فایل‌های dll مورد استفاده true باشد. البته این حالت را در شرایط بالا قرار داده تست کرده بودم ولی مشکل همچنان پابرجا بود؛ ولی به عنوان نکته می‌توان در نظر گرفت.
      <ItemGroup>
          <None Update="DLL\LC.dll">
            <CopyToOutputDirectory>Always</CopyToOutputDirectory>
          </None>
      </ItemGroup>

      نکته اصلی که مشکل را حل کرد حذف فیزیکی پوشه‌های bin و obj از پروژه بود! که با بیلد مجدد، دوباره تشکیل شده و در زمان اجرا دیگر خطایی وجود نداشت.