فرض کنید یک سرور را بر روی اینترنت قرار دادهاید و از SMTP Server متعلق به IIS قصد دارید جهت ارسال ایمیل توسط برنامههای خود استفاده نمائید. در این حالت مواردی را باید رعایت نمود تا این سرور تبدیل به سرور رایگان ارسال spam توسط "دشمنان" نشود.
1- پورت پیش فرض را عوض کنید
پورت پیش فرض اتصال به SMTP Server مساوی 25 است. از آنجائیکه به سادگی در برنامههای خود میتوان این پورت را نیز تنظیم نمود، بهتر است به عنوان اولین قدم، این پورت را تغییر داد. یک شماره پورت دلخواه خالی را یافته و بجای 25 قرار دهید. برای این منظور مسیر زیر را طی کنید:
بر روی Default SMTP Virtual Server در کنسول IIS کلیک راست کرده و گزینه خواص را انتخاب کنید. در برگه General روی دکمه Advanced کلیک کرده و در صفحه باز شده سطر مربوط به پورت 25 را یافته، بر روی دکمه Edit کلیک نموده و آنرا به عددی دیگر تغییر دهید.
2- دسترسی عموم را به سرور قطع کنید!
متاسفانه تنظیمات پیش فرض SMTP Server متعلق به IIS در جهت قطع دسترسی "دشمنان" کاملا نادرست بوده و بر مبنای ایده حداقل دسترسی صورت نگرفته است. اگر سرور را به این حال رها کنید فقط "دشمنان" را خوشحال کردهاید.
برای قطع دسترسی دشمنان سه مرحله باید صورت گیرد:
الف) در برگه Access مربوط به تنظیمات SMTP server ، روی دکمه relay کلیک کرده، ابتدا تیک مربوط به Allow all computers which successfully authenticated to relay را بردارید (این مورد در یک شبکه داخلی حائز اهمیت میشود و سایر کامپیوترها را منع میکند). سپس در قسمت بالای صفحه گزینه only the list below را انتخاب کرده و IP آنرا مساوی 127.0.0.1 وارد کنید (یعنی فقط این کامپیوتر مجاز است که از این سرویس جهت ارسال ایمیل استفاده کند؛ نه دشمنان خارجی).
ب) مورد الف را دربارهی قسمت مرتبط با دکمه connections نیز تکرار کنید. (پیش فرض آن تمام عالم است!)
ج) در همین برگهی Access بر روی دکمه Authentication کلیک کرده و فقط تیک مربوط به integrated windows authentication را قرار دهید. (همیشه تحت ویندوز این روش authentication یکی از امنترینها است. همچنین در حالت قرارگیری سرور بر روی اینترنت سخت گیرانهترین حالت ممکن را در اینجا انتخاب کردهایم.)
خوب، با این تنظیم قسمت (ج) دیگر برنامهها با روش متداول قابل به ارسال ایمیل نخواهند بود. یک یوزر معمولی local را به کامپیوتر افزوده (با حداقل دسترسی) و پسورد آنرا در حالت never expires قرار دهید. از این یوزر ویندوزی جهت برقراری امکان اتصال به میل سرور محلی در برنامههای خود استفاده خواهیم کرد (فرض بر این است که برنامهای هم که قرار است ایمیل ارسال کند بر روی همان کامپیوتر سرور قرار دارد).
پس از اعمال این تنظیمات بر روی دکمه apply کلیک کنید، تا تنظیمات اعمال شوند. یکبار نیز میل سرور را استاپ و استارت کنید.
3- تنظیمات ویژه برنامهها برای ارسال ایمیل:
در این حالت برنامههای دات نت شما نیاز به چهار تنظیم اضافهتر پیش از فراخوانی تابع Send دارند:
MailMessage message = new MailMessage("from@site.com", strTo, subject, body)
{
IsBodyHtml = true,
BodyEncoding = Encoding.UTF8
};
SmtpClient client = new SmtpClient("127.0.0.1",portNumber);//portNumber is new
client.UseDefaultCredentials = false; //new
client.DeliveryMethod = SmtpDeliveryMethod.Network; //new
client.Credentials = new NetworkCredential("mail_user", "pass"); //new
client.Send(message);
4- تمامی رخدادهای میل سرور را ثبت کنید.
برای این منظور در برگه general ، تیک مربوط به enable logging را فعال کنید. سپس بر روی دکمه خواص کنار آن کلیک کرده و در صفحه باز شده به برگه extended properties مراجعه نموده و تمامی موارد را تیک بزنید. به ازای هر یک روز فعالیت سرور، یک فایل متنی در مسیر C:\WINDOWS\System32\LogFiles تشکیل خواهد شد.
سؤال چگونه تشخیص دهم که میل سرور من هک شده است یا خیر؟!
اگر موارد فوق را رعایت نکنید، در قسمت current sessions کنسول IIS میتوانید "دشمنان" را مشاهده کنید! همچنین مصرف CPU پروسه inetinfo.exe عملکرد سرور را مختل کرده، بعلاوه در مسیر C:\Inetpub\mailroot\Queue احتمالا چند هزار ایمیل درصف قرار گرفته شده برای ارسال را میتوانید مشاهده کنید! (همینطور در مسیر C:\Inetpub\mailroot\Badmail نیز این تعرض قابل مشاهده است)
اگر این موارد را مشاهده کردید، ابتدا سرور را استاپ کنید، سپس محتویات پوشههای یاد شده را تخلیه کرده و از مرحله یک فوق شروع به اعمال تنظیمات نمائید.
<button data-perm="true" data-controller="c" data-action="r" data-method="post" data-type="disable">Test 1</button> <button data-perm="true" data-controller="c" data-action="t" data-method="post">Test 2</button> <button data-perm="false" data-controller="c" data-action="r" data-method="post">Test 3</button> <a data-perm="true" data-controller="c" data-action="m" data-method="post">Test 4</a> <a data-perm="false" data-controller="c" data-action="m" data-method="post">Test 5</a> <a data-perm="true" data-controller="c" data-action="t" data-method="post">Test 6</a>
ویژگی | توضیحات |
perm | آیا نیاز به اعتبارسنجی دارد یا خیر؟ در صورتی که المانی مقدار Perm آن با مقدار true پر گردد، اعتبارسنجی روی آن اعمال خواهد شد. |
controller | نام کنترلری که به آن دسترسی دارد. |
action | نام اکشنی که به آن در کنترلر ذکر شده دسترسی دارد. |
method | در صورتیکه دسترسی get و post و ... هر یک متفاوت باشد. |
type | نحوه برخورد با المان غیرمجاز. در صورتیکه با disable مقداردهی شود، المان غیرفعال و در غیر اینصورت، از روی صفحه حذف میشود. |
سپس یک کلاس جدید ساخته و با ارث بری از ActionFilterAttribute، کار ساخت فیلتر را آغاز میکنیم:
public class AuthorizePage: ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; IAuthorization _auth; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter) filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; _auth = new Auth(); } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); response = AuthorizeTags(response); _output.Write(response); } public string AuthorizeTags(string response) { var doc = GetHtmlDocument(response); var nodes=doc.DocumentNode.SelectNodes("//*[@data-perm]"); if (nodes == null) return response; foreach(var node in nodes) { var dataPermission = node.Attributes["data-perm"]; if(!dataPermission.Value.TryBooleanParse()) { continue; } var controller = node.Attributes["data-controller"].Value; var action = node.Attributes["data-action"].Value; var method = node.Attributes["data-method"].Value; var access=_auth.Authorize(HttpContext.Current.User.Identity.Name , controller, action, method); if (access) continue; var removeElm = true; var type = node.Attributes["data-type"]?.Value; if (type!=null && type.ToLower()== "disable") { removeElm = false; } if(removeElm) { node.Remove(); continue; } node.Attributes.Add("disabled", "true"); } return doc.DocumentNode.OuterHtml; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
public interface IAuthorization { bool Authorize(string userId, string controller, string action, string method); }
ادامه کد در متد OnResultExecuted قرار دارد و متد اصلی کار ما میباشد. این متد بعد از صدور خروجی از اکشن، صدا زده شده اجرا میشود و شامل خروجی اکشن میباشد. خروجی اکشن را به متدی به نام AuthorizeResponse داده و با استفاده از بسته htmlagilitypack که یک HTML Parser میباشد، کدهای HTML را تحلیل میکنیم. قاعده فیلترسازی المانها در این کتابخانه بر اساس قواعد تعریف شده در XPath میباشد. بر اساس این قاعده ما گفتیم هر نوع تگی که دارای ویژگی data-perm میباشد، باید به عنوان گرههای فیلتر شده برگشت داده شود. سپس مقادیر نام کنترلر و اکشن و ... از المان دریافت شده و با استفاده از اینترفیسی که ما اینجا تعریف کردهایم، بررسی میکنیم که آیا این کاربر به این موارد دسترسی دارد یا خیر. در صورتیکه پاسخ برگشتی، از عدم اعتبار کاربر بگوید، گره مورد نظر حذف و یا در صورتیکه ویژگی data-type وجود داشته و مقدارش برابر disable باشد، آن المان غیرفعال خواهد شد. در نهایت کد تولیدی سند را به رشته تبدیل کرده و جایگزین خروجی فعلی میکنیم.
protected void Application_Start() { GlobalFilters.Filters.Add(new AuthorizePage()); }
آغاز کار با الکترون
فرآیندها (Processes) در الکترون به دو بخش تقسیم میشوند:
یک. فرآیند اصلی (Main Process) که همان فایل جاوااسکریپتی است و توسط main، در فایل package.json مشخص شدهاست .فرآیند اصلی تنها فرآیندی است که قابلیت دسترسی به امکانات گرافیکی سیستم عامل را از قبیل نوتیفیکشن ها، دیالوگها ،Tray و ... دارد. فرآیند اصلی میتواند با استفاده از شیء BrowserWindow که در قسمت قبلی کاربرد آن را مشاهده کردیم، render process را ایجاد کند. با هر بار ایجاد یک نمونه از این شیء، یک Render Process ایجاد میشود.
دو. فرآیند رندر (Render Process): از آنجا که الکترون از کرومیوم استفاده میکند و کرومیوم شامل معماری چند پردازشی است، هر صفحهی وب میتواند پردازش خود را داشته باشد که به آن Render Process میگویند. به طور معمول در مرورگرها، صفحات وب در محیطی به نام SandBox اجرا میشوندکه اجازه دسترسی به منابع بومی را ندارند. ولی از آنجا که الکترون میتواند از Node.js استفاده کند، قابلیت دسترسی به تعاملات سطح پایین سیستم عامل را نیز داراست.
در فرآیند اصلی، پنجرهها توسط BrowserWindow ایجاد میشوند و هر پنجرهای که صفحه وبی را برای خودش باز میکند، شامل Render Process خودش است و هر پنجرهای که کارش خاتمه یابد، فرآیند مربوط به خودش به اتمام میرسد. فرآیند اصلی، همه صفحات وب به همراه Render Process مربوط به خودشان را مدیریت میکند و هر فرآیند رندر، از دیگری مجزا و محافظت شده است و تنها تمرکزش بر روی صفحه وبی است که متعلق به خودش است.
در ابتدا قصد داریم یک منو برای برنامهی خود درست کنیم. برای ساخت منو، راههای متفاوتی وجود دارند که فعلا ما راه استفاده از template را بر میگزینیم که به صورت یک آرایه نوشته میشود. کدهای زیر را در فایل index.js یا هر اسمی که برای آن انتخاب کردهاید بنویسید:
const electron = require('electron'); const {app,dialog,BrowserWindow,Menu,shell} = electron; let win; app.on('ready', function () { win = new BrowserWindow({width: 800, height: 600}); win.loadURL(`file://${__dirname}/index.html`); var app_menu=[ { label:'پرونده', submenu:[ { label:'باز کردن', accelerator:'CmdOrCtrl+O', click:()=>{ } }, { label:'ذخیره', accelerator:'CmdOrCtrl+S', click:()=>{ } } ] }, { label:'سیستم', submenu:[ { label:'درباره ما', click:()=> { shell.openExternal('https://www.dntips.ir'); } }, { label:'خروج', accelerator:'CmdOrCtrl+X', click:()=> { win=null; app.quit(); } } ] } ];
در خطوط بعدی، یک کار اضافهتر را جهت آشنایی بیشتر انجام میدهیم. قصد داریم اگر سیستم عامل مکینتاش بود، نام برنامه هم در ابتدای نوار منو نمایش داده شود. به همین جهت در ادامه خطوط زیر را اضافه میکنیم:
if(process.platform=="darwin") { const app_name=app.getName(); app_menu.unshift({ label:app_name }) }
ویندوز | win32 حتی اگر 64 بیتی باشد. |
لینوکس | linux |
مک | darwin |
فری بی اس دی | freebsd |
سولاریس | sunos |
دستو shell در بالا به شما اجازه میدهد با محیط دسکتاپ، یکپارچگی خود
را حفظ کنید و دستوراتی از قبیل باز کردن url، باز کردن یک مسیر دایرکتوری،
باز کردن یک فایل، انتقال فایل به سطل آشغال یا بازیافت و صدای بوق سیستم
(بیپ) را به شما میدهد. مستندات این شیء را در اینجا مطالعه فرمایید.
{ label:'خروج', accelerator:'CmdOrCtrl+X', role:'close' }
در انتها با اجرای دو دستور زیر، منو ساخته میشود:
var menu=Menu.buildFromTemplate(app_menu); Menu.setApplicationMenu(menu);
حال قصد داریم برای زیرمنوی «باز کردن فایل» یک دیالوگ open درخواست کنیم. برای این کار از شیء dialog استفاده میکنیم. پس خطوط زیر را به رویداد کلیک این زیرمنو اضافه میکنیم:
dialog.showOpenDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']}, {name:'جهت تست' , extensions:['doc','docx']} ] }, (filename)=>{ if(filename===undefined) return; dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`}); });
برای قسمت ذخیره هم کد زیر را مینویسیم:
dialog.showSaveDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']} ] }, (filename)=>{ if(filename===undefined) return; });
حال بهتر است این دیالوگهای جاری را هدفمند کنیم و بتوانیم فایلهای متنی را به کاربر نمایش دهیم، یا آنها را ذخیره کنیم. به همین علت فایل html زیر را نوشته و طبق دستوری که در مقاله «آشنایی با الکترون» فرا گرفتیم، آن را نمایش میدهیم:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title></title> </head> <body> Fie Content:<br/> <textarea id="TextFile" cols="100" rows="50"></textarea> </body> </html>
کاری که میخواهیم انجام دهیم این است که فایل متنی را باز کرده و محتوای آن را در کادر متنی نشان دهیم و موقع ذخیره نیز محتوای نوشته شده در کادر متنی را در فایلی ذخیره کنیم. از آنجا که main Process به المانهای DOM یا Render Process دسترسی ندارد، باید از طریقی، ارتباط آن را برقرار کنیم. یکی از راههای برقراری این ارتباط، IPC است. IPC در واقع یک فرستنده و یک شنونده است که هر کدام در یک سمت قرار گرفته اند. فرستنده پیام را تحت یک عنوان ارسال میکند و شنونده منتظر دریافت پیامی تحت همان عنوان میماند و پیام دریافتی را پاسخ میدهد. در این مقاله، ما فقط قسمتی از این نوع ارتباطات را بررسی میکنیم.
در نتیجه محتوای callback کدهای دیالوگ open و save را به شکل زیر تغییر میدهیم:
Open
dialog.showOpenDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']}, {name:'جهت تست' , extensions:['doc','docx']} ] }, (filename)=>{ if(filename===undefined) return; win.webContents.send('openFile',filename); // dialog.showMessageBox({title:'پیام اطلاعاتی',type:"info",buttons:['تایید'],message:`the name of file is [${filename}]`}); });
dialog.showSaveDialog({ title:'باز کردن فایل متنی', properties: [ 'openFile']//[ 'openFile', 'openDirectory', 'multiSelections' ] ,filters:[ {name:'فایلهای نوشتاری' , extensions:['txt','text']} ] }, (filename)=>{ if(filename===undefined) return; win.webContents.send('saveFile',filename); });
برای ایجاد شنونده هم کد زیر را به فایل index.html اضافه میکنیم:
<script> const {ipcRenderer} = require('electron'); var fs=require('fs'); ipcRenderer.on('openFile', (event, arg) => { var content= fs.readFileSync(String(arg),'utf8'); document.getElementById("TextFile").value=content; }); ipcRenderer.on('saveFile', (event, arg) =>{ var content=document.getElementById("TextFile").value; fs.writeFileSync(String(arg),content,'utf8'); alert('ذخیره شد'); }); </script>
در اینجا شوندههایی را از نوع ipcRenderer ایجاد میکنیم و با استفاده از متد on، به پیامهایی تحت عنوانهای مشخص شده گوش فرا میدهیم. پیامهای ارسالی را که حاوی آدرس فایل میباشند، به شیءای که از نوع fs میباشد، میدهند و آنها را میخوانند یا مینویسند. خواندن و نوشتن فایل، به صورت همزمان صورت میگیرد. ولی اگر دوست دارید که به صورت غیر همزمان پیامی را بخوانید یا بنویسید، باید عبارت Sync را از نام متدها حذف کنید و یک callback را به عنوان پارامتر دوم قرار دهید و محتوای آن را از طریق نوشتن یک پارامتر در سازنده دریافت کنید.
فایلهای پروژه
- توابع خط فرمان - Command Line API
- توابع کنسول - Console API
<div id="first" class="content">Content1 with css class and id</div> <div class="content"> Content2 with css class <a class="links" href="#">Link1</a> <a href="#">Link2</a> </div> <div> Content3 without css class and id </div> <input type="button" onclick="myFunc()" value="Run myFunc" /> <input type="text" id="myInput" /> <script type="text/javascript"> function myFunc() { loop(1000); loop(50000); } function loop(number) { for (var i = 0; i < number; i++) { } } </script>
$$("div.content");
profile("myFunc Testing"); myFunc(); profileEnd();
- $(id)
معادل دستور document.getElementById است که یک المنت با id داده شده بر میگرداند .
$("first");
- $$(selector)
آرایه ای از المنتهای مطابق با selector داده شده بر میگرداند .
$$("div.content")
به تفاوت دو دستور توجه کنید . خروجی دستور اول ، یک المنت است و خروجی دستور دوم یک آرایه از المنت که بین [ و ] قرار گرفته اند .
برای آشنایی بیشتر با CSS Seletorها به این لینک مراجعه کنید : http://www.w3.org/TR/css3-selectors
- $x(xPathExpression)
آرایه ای از المنت هایی را بر میگرداند که با xPath داده شده مطابقت داشته باشند .
var objects = $x("html/body/div[2]/a") for(var i = 0; i < objects.length; i++) { console.log(objects[i]); }
برای آشنایی بیشتر با عبارات xPath به این لینک مراجعه کنید : http://www.w3schools.com/xpath
- dir(object)
تمام خصوصیات شیء ارسال شده را لیست میکند .
var objects = $x("html/body/div[2]/a") dir(objects);
- dirxml(node)
سورس یک المنت را بصورت درختواره ( tree ) پرینت میکند . همچنین با کلیک بروی هر node ، فایرباگ آن node را در تب html نمایش میدهد .
var node = $("first"); dirxml(node);
توجه کنید که این دستور فقط یک node دریافت میکند . برای همین اگر از دستور $$("#first") استفاده میکنید ، چون این دستور یک آرایه بر میگرداند ، باید اولین عضو آرایه را دریافت و ارسال کنید .
یعنی :
var node = $$("#first")[0]; dirxml(node);
- clear()
این دستور محیط console را خالی میکند . عملکرد این دستور معادل کلیک دکمهی Clear ( در بالا - چپ تب کنسول ) است .
- inspect(object[,tabName])
توسط این دستور میتوانید یک شیء را در مناسبترین تب فایرباگ یا یکی از تبهای مورد نظر خود ، Inspect کنید .
var node = $("first"); inspect(node); // inspect in html tab inspect(node,'dom'); // inspect in dom tab
- keys(object)
آرایه ای از "نام" تمام خصوصیات شیء ارسال شده بر میگرداند .
var obj = $("first"); keys(obj)
- values(object)
آرایه ای از "مقدار" تمام خصوصیات شیء ارسال شده بر میگرداند .
var obj = $("first"); values(obj)
- debug(fn) and undebug(fn)
این متدها یک BreakPoint در ابتدای تابع مشخص شده اضافه/حذف میکنند . ( در تب Script ) . به همین ترتیب هنگامی که تابع مورد نظر فراخوانی شود ، در نقطه ای که BreakPoint قرار داده شده توقف خواهد کرد .
البته میشود BreakPoint را دستی هم قرار داد . در اصل این تابع ، این عملیات را سادهتر میکند .
debug(myFunc); myFunc(); undebug(myFunc);
- monitor(fn) and unmonitor(fn)
این متدها برای فعال/غیرفعال کردن Logging فراخوانیهای یک تابع استفاده میشوند .
در حالت عادی برای پی بردن به اینکه یک تابع اجرا میشود یا نه ، در تابع مورد نظر یک alert قرار میدهیم و تست میکنیم . که این روش در برنامه برنامههای بزرگ صحیح نیست . زیرا در این حالت باید بین حجم زیادی کد به دنبال تابع مورد نظر بگردیم و سپس alert را قرار بدهیم و بعد اطمینان از صحت عملکرد تابع مجدد آن را حذف کرد ، که با اتلاف زمان و به خطر انداختن کدها همراه است .
اما با استفاده از این متدها ، تنها نیاز به داشتن اسم تابع داریم ( و نه مکان تابع در کدهای برنامه ) .
تست monitor :
monitor(myFunc); // now click on "Run myFunc" button
unmonitor(myFunc); // now click on "Run myFunc" button
- monitorEvents(object[, types]) and unmonitorEvents(object[, types])
این متدها عملیات Event Logging برای یک شیء را فعال/غیرفعال میکنند . در کنار شیء مورد نظر ، میتوان نوع رویداد را هم به متد ارسال کرد . در این صورت عملیات Logging فقط برای همان گروه رویداد/رویداد ، فعال/غیرفعال میشود .
منظور از گروه رویداد ، مجموعه رویدادهای یک شیء است . مثلا mousemove , moseover , mousedown , ... در گروه mouse قرار میگیرند . یعنی میتوانید با ارسال کلمهی mouse فقط رویدادهای mouse را تحت نظر بگیرید یا اینکه فقط یک رویداد را مشخص کنید ، مثل mousedown .
راه سادهتر فعال کردن Event Logging ، رفتن به تب Html ، راست کلیک کردن بروی المنت مورد نظر و فعال کردن گزینهی Log Events میباشد .
var obj = $("myInput"); monitorEvents(obj,'keypress');
نتیجه پس از فشردن چند دکمهی کیبورد در myInput :
توضیحات بیشتر : http://getfirebug.com/wiki/index.php/MonitorEvents - profile([title]) and profileEnd()
این متدها ، JavaScript Profiler را فعال/غیرفعال میکنند . هنگام فعال کردن میتوانید یک عنوان هم برای پروفایل مشخص کنید . در قسمت قبلی مقاله در مورد این قابلیت توضیحاتی ارائه شد .
سه را برای اجرای Profiler وجود دارد :
1 - کلیک بروی دکمهی Profiler در بالای تب کنسول .
2 - استفاده از کد console.profile("ProfileTitle") در کدهای جاوا اسکریپت .
3 - استفاده از متد profile("Profile Title") در خط فرمان .
profile("myFunc Testing"); myFunc(); profileEnd();
ستونهای Profiler :Function : نام تابع اجرا شده .
Calls : تعداد دفعات فراخوانی تابع .
Percent : زمان اجرای تابع در زمان کل ، به درصد .
Own Time : زمان اجرای تابع به تنهایی . برای مثال در کد ما ، زمان اجرای تابع myFunc به تنهایی تقریبا صفر است زیرا عمیاتی در خود انجام نمیدهد و زمان صرف شده در این تابع ، برای اجرای 2 با تابع loop است . این زمان ( Own Time ) زمان اجرای تابع ، منهای زمان صرف شده برای فراخوانی توابع دیگر است .
Time : زمان اجرای تابع از نقطهی آغاز تا پایان . مجموع زمان اجرای خود تابع به همراه زمان اجرای توابع فراخوانی شده . در کد ما ، این زمان ، مجموع زمان اجرای خود تابع به همراه دو بار فراخوانی تابع loop است .
Avg : میانگین زمان اجرای هربار تابع . فرمول : Avg = Time / Calls
Min & Max : حداقل و حداکثر زمان اجرای تابع .
File : نام فایل و شماره خطی که تابع در آن قرار دارد .
شیوهنامههای کار با دکمهها در بوت استرپ 4
کلاس پایه ایجاد دکمههای بوت استرپی، کلاس btn است. البته آنرا میتوان با کلاسهای دیگری نیز ترکیب کرد:
- کلاس btn را میتوان بر روی المانهایی مانند anchor، button و input نیز اعمال کرد:
<a class="btn btn-primary" href="#" role="button">Link</a> <button class="btn btn-primary" type="submit">Button</button> <input class="btn btn-primary" type="submit" value="Input">
- برای تعیین رنگ اجباری دکمههای بوت استرپ، از فرمول زیر استفاده میشود؛ مانند btn-primary:
<button class="btn btn-primary">Primary</button> <button class="btn btn-secondary">Secondary</button> <button class="btn btn-success">Success</button> <button class="btn btn-danger">Danger</button> <button class="btn btn-warning">Warning</button> <button class="btn btn-info">Info</button> <button class="btn btn-light">Light</button> <button class="btn btn-dark">Dark</button>
همانطور که ملاحظه میکنید، رنگ این دکمهها نیز نسبت به نگارش سوم آن، به روز رسانی شدهاست.
همچنین نگارش outline آنها نیز قابل تعریف است؛ مانند btn-outline-primary:
<button class="btn btn-outline-primary">Primary</button> <button class="btn btn-outline-secondary">Secondary</button> <button class="btn btn-outline-success">Success</button> <button class="btn btn-outline-danger">Danger</button> <button class="btn btn-outline-warning">Warning</button> <button class="btn btn-outline-info">Info</button> <button class="btn btn-outline-light">Light</button> <button class="btn btn-outline-dark">Dark</button>
- کلاس btn-size که در اینجا size میتواند sm یا lg باشد و سبب ایجاد دکمههایی کوچک یا بزرگ میشوند.
- کلاس btn-block سبب میشود تا دکمهای کل عرض container را پر کند.
<button class="btn btn-primary">Default</button> <button class="btn btn-primary btn-lg">Large</button> <button class="btn btn-primary btn-sm">Small</button> <button class="btn btn-primary btn-block">Block</button>
- امکان اعمال کلاسهای active و disabled نیز در اینجا میسر است:
<h2>States</h2> <h3>Active</h3> <button class="btn btn-primary active">Active Button</button> <h3>Disabled</h3> <button class="btn btn-primary disabled">Disabled Button</button> <a class="btn btn-primary disabled" href="#">Disabled Link Button</a>
تشکیل گروهی از دکمهها در بوت استرپ 4
برای تبدیل تعدادی از دکمهها به یک گروه، از کلاس btn-group استفاده میشود. همچنین امکان تشکیل گزینهی عمودی آن، با کلاس btn-group-vertical نیز وجود دارد. در این حالت دکمهها بجای نمایش افقی، به صورت یک ستون نمایش داده میشوند. کلاس btn-toolbar نیز برای تشکیل نوار ابزاری از دکمهها در نظر گرفته شدهاست. توسط کلاسهای btn-group-sm و یا btn-group-lg میتوان اندازهی این گروهها را تغییر داد.
<div class="btn-toolbar" aria-label="All pets"> <div class="btn-group mb-2 mr-2" aria-label="Common pets"> <button type="button" class="btn btn-primary active">Cat</button> <button type="button" class="btn btn-primary">Dog</button> <button type="button" class="btn btn-primary">Fish</button> <button type="button" class="btn btn-primary">Bird</button> </div> <div class="btn-group mb-2" aria-label="Exotic pets"> <button type="button" class="btn btn-primary">Amphibian</button> <button type="button" class="btn btn-primary active">Reptile</button> <button type="button" class="btn btn-primary">Other</button> </div> </div>
در اینجا دو گروه از دکمهها تشکیل شدهاند که اینها را داخل یک btn-toolbar قرار دادهایم. همچنین تعریف aria-labelها به screen readers و معلولین کمک میکند.
به علاوه با استفاده از کلاسهای mb-2 و mr-2، سبب ایجاد margin بین این نوار ابزار با متن زیر آن و همچنین بین خود آنها شدهایم.
و حالت عمودی آن:
<div class="btn-group-vertical mb-2" aria-label="Exotic pets"> <button type="button" class="btn btn-primary">Amphibian</button> <button type="button" class="btn btn-primary active">Reptile</button> <button type="button" class="btn btn-primary">Other</button> </div>
کلاسهای جدید تشکیل Badges در بوت استرپ 4
برای تشکیل نشان/آرم از کلاس badge استفاده میشود و برای تغییر شکل آن میتوان از کلاس badge-pill کمک گرفت. برای تغییر رنگ آن نیز فرمول زیر وجود دارد:
یک مثال:
<div class="container"> <div class="row"> <section class="col-12"> <h1>Our Commitment <span class="badge badge-info">to you</span></h1> <h3>Grooming <span class="badge badge-danger badge-pill">new</span></h3> </section> </div><!-- row --> </div><!-- content container -->
همانطور که مشاهده میکنید، یک badge همواره به اندازهی container آن در آمده و نمایش داده میشود.
ایجاد لیستی از آیتمها در بوت استرپ 4
بوت استرپ، کلاسهایی را برای ایجاد لیستهایی با ظاهر لیستهای اندرویدی به همراه دارد که در ادامه با مثالهایی آنها را بررسی خواهیم کرد.
<ul class="list-group mb-3"> <li class="list-group-item active">Grooming</li> <li class="list-group-item list-group-item-action"> General Health </li> <li class="list-group-item list-group-item-action">Nutrition</li> <li class="list-group-item list-group-item-action"> Pest Control </li> <li class="list-group-item list-group-item-action">Vaccinations</li> </ul>
در اینجا برای تشکیل لیستی با ظاهری شکیلتر، میتوان ابتدا کلاس list-group را به ul انتساب داد و سپس به هر کدام از liهای آن کلاس list-group-item انتساب داده میشود. با افزودن کلاس active به اولین مورد، ظاهر آن با رنگی متمایز نمایان میشود. همچنین اگر علاقمند بودیم تا هر کدام از آیتمها با عبور ماوس بر روی آنها، با رنگ ملایمی مشخص شوند، میتوان از کلاس list-group-item-action استفاده کرد.
این list-group را بر روی لینکها و دکمهها نیز میتوان همانند قبل اعمال کرد:
<div class="list-group mb-3"> <a class="list-group-item active" href="#">Grooming</a> <a class="list-group-item list-group-item-action list-group-item-success" href="#">Nutrition</a> <a class="list-group-item list-group-item-action list-group-item-info" href="#"> Pest Control </a> <a class="list-group-item list-group-item-action list-group-item-warning" href="#">Vaccinations</a> </div>
در اینجا از کلاسهایی مانند list-group-item-warning برای تغییر رنگ پس زمینهی هر آیتم میتوان کمک گرفت.
برای تعریف badge برای هر آیتم، میتوان به صورت زیر عمل کرد:
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"> Nutrition <span class="badge badge-info badge-pill">12</span> </li>
با اعمال کلاسهای badge، این نشان نمایش داده میشود؛ اما دقیقا در کنار متن Nutrition در اینجا. برای اینکه آنرا به سمت دیگر این ردیف منتقل و همچنین تراز عمودی آنرا نیز به میانهی صفحه تنظیم کنیم، میتوان از Flexbox کمک گرفت. با اعمال d-flex، این ردیف تبدیل به یک Flexbox container میشود و سپس میتوان کلاسهای مخصوص Flexbox مانند justify-content-between و align-items-center را به این ردیف اعمال کرد تا ترازهای عمودی و افقی آیتمهای قرار گرفتهی درون آن تغییر کنند.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: Bootstrap4_08.zip
Roslyn #6
پیشنیاز این بحث نصب مواردی است که در مطلب «شروع به کار با Roslyn » در قسمت دوم عنوان شدند:
الف) نصب SDK ویژوال استودیوی 2015
ب) نصب قالبهای ایجاد پروژههای مخصوص Roslyn
البته این قالبها چیزی بیشتر از ایجاد یک پروژهی کلاس Library جدید و افزودن ارجاعاتی به بستهی نیوگت Microsoft.CodeAnalysis، نیستند. اما درکل زمان ایجاد و تنظیم این نوع پروژهها را خیلی کاهش میدهند و همچنین یک پروژهی تست را ایجاد کرده و تولید بستهی نیوگت و فایل VSIX را نیز بسیار ساده میکنند.
هدف از تولید Analyzers
بسیاری از مجموعهها و شرکتها، یک سری قوانین و اصول خاصی را برای کدنویسی وضع میکنند تا به کدهایی با قابلیت خوانایی بهتر و نگهداری بیشتر برسند. با استفاده از Roslyn و آنالیز کنندههای آن میتوان این قوانین را پیاده سازی کرد و خطاها و اخطارهایی را به برنامه نویسها جهت رفع اشکالات موجود، نمایش داده و گوشزد کرد. بنابراین هدف از آنالیز کنندههای Roslyn، سهولت تولید ابزارهایی است که بتوانند برنامه نویسها را ملزم به رعایت استانداردهای کدنویسی کنند.
همچنین معلمها نیز میتوانند از این امکانات جهت ارائهی نکات ویژهای به تازهکاران کمک بگیرند. برای مثال اگر این قسمت از کد اینگونه باشد، بهتر است؛ مثلا بهتر است فیلدهای سطح کلاس، خصوصی تعریف شوند و امکان دسترسی به آنها صرفا از طریق متدهایی که قرار است با آنها کار کنند صورت گیرد.
این آنالیز کنندها به صورت پویا در حین تایپ کدها در ویژوال استودیو فعال میشوند و یا حتی به صورت خودکار در طی پروسهی Build پروژه نیز میتوانند ظاهر شده و خطاها و اخطارهایی را گزارش کنند.
بررسی مثال معتبری که میتواند بهتر باشد
در اینجا یک کلاس نمونه را مشاهده میکنید که در آن فیلدهای کلاس به صورت public تعریف شدهاند.
public class Student { public string FirstName; public string LastName; public int TotalPointsEarned; public void TakeExam(int pointsForExam) { TotalPointsEarned += pointsForExam; } public void ExtraCredit(int extraPoints) { TotalPointsEarned += extraPoints; } public int PointsEarned { get { return TotalPointsEarned; } } }
بنابراین در ادامه هدف ما این است که یک Roslyn Analyzer جدید را طراحی کنیم تا از طریق آن هشدارهایی را جهت تبدیل فیلدهای عمومی به خصوصی، به برنامه نویس نمایش دهیم.
با اجرای افزونهی View->Other windows->Syntax visualizer، تصویر فوق نمایان خواهد شد. بنابراین در اینجا نیاز است FieldDeclarationها را یافته و سپس tokenهای آنها را بررسی کنیم و مشخص کنیم که آیا نوع یا Kind آنها public است (PublicKeyword) یا خیر؟ اگر بلی، آن مورد را به صورت یک Diagnostic جدید گزارش میدهیم.
ایجاد اولین Roslyn Analyzer
پس از نصب پیشنیازهای بحث، به شاخهی قالبهای extensibility در ویژوال استودیو مراجعه کرده و یک پروژهی جدید از نوع Analyzer with code fix را آغاز کنید.
قالب Stand-alone code analysis tool آن دقیقا همان برنامههای کنسول بحث شدهی در قسمتهای قبل است که تنها ارجاعی را به بستهی نیوگت Microsoft.CodeAnalysis به صورت خودکار دارد.
قالب پروژهی Analyzer with code fix علاوه بر ایجاد پروژههای Test و VSIX جهت بسته بندی آنالایزر تولید شده، دارای دو فایل DiagnosticAnalyzer.cs و CodeFixProvider.cs پیش فرض نیز هست. این دو فایل قالبهایی را جهت شروع به کار تهیهی آنالیز کنندههای مبتنی بر Roslyn ارائه میدهند. کار DiagnosticAnalyzer آنالیز کد و ارائهی خطاهایی جهت نمایش به ویژوال استودیو است و CodeFixProvider این امکان را مهیا میکند که این خطای جدید عنوان شدهی توسط آنالایزر، چگونه باید برطرف شود و راهکار بازنویسی Syntax tree آنرا ارائه میدهد.
همین پروژهی پیش فرض ایجاد شده نیز قابل اجرا است. اگر بر روی F5 کلیک کنید، یک کپی جدید و محصور شدهی ویژوال استودیو را باز میکند که در آن افزونهی در حال تولید به صورت پیش فرض و محدود نصب شدهاست. اکنون اگر پروژهی جدیدی را جهت آزمایش، در این وهلهی محصور شدهی ویژوال استودیو باز کنیم، قابلیت اجرای خودکار آنالایزر در حال توسعه را فراهم میکند. به این ترتیب کار تست و دیباگ آنالایزرها با سهولت بیشتری قابل انجام است.
این پروژهی پیش فرض، کار تبدیل نام فضاهای نام را به upper case، به صورت خودکار انجام میدهد (که البته بیمعنا است و صرفا جهت نمایش و ارائهی قالبهای شروع به کار مفید است).
نکتهی دیگر آن، تعریف تمام رشتههای مورد نیاز آنالایزر در یک فایل resource به نام Resources.resx است که در جهت بومی سازی پیامهای خطای آن میتواند بسیار مفید باشد.
در ادامه کدهای فایل DiagnosticAnalyzer.cs را به صورت ذیل تغییر دهید:
using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace CodingStandards { [DiagnosticAnalyzer(LanguageNames.CSharp)] public class CodingStandardsAnalyzer : DiagnosticAnalyzer { public const string DiagnosticId = "CodingStandards"; // You can change these strings in the Resources.resx file. If you do not want your analyzer to be localize-able, you can use regular strings for Title and MessageFormat. internal static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.AnalyzerTitle), Resources.ResourceManager, typeof(Resources)); internal static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.AnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); internal static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.AnalyzerDescription), Resources.ResourceManager, typeof(Resources)); internal const string Category = "Naming"; internal static DiagnosticDescriptor Rule = new DiagnosticDescriptor( DiagnosticId, Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: true, description: Description); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } } public override void Initialize(AnalysisContext context) { // TODO: Consider registering other actions that act on syntax instead of or in addition to symbols context.RegisterSyntaxNodeAction(analyzeFieldDeclaration, SyntaxKind.FieldDeclaration); } static void analyzeFieldDeclaration(SyntaxNodeAnalysisContext context) { var fieldDeclaration = context.Node as FieldDeclarationSyntax; if (fieldDeclaration == null) return; var accessToken = fieldDeclaration .ChildTokens() .SingleOrDefault(token => token.Kind() == SyntaxKind.PublicKeyword); // Note: Not finding protected or internal if (accessToken.Kind() != SyntaxKind.None) { // Find the name of the field: var name = fieldDeclaration.DescendantTokens() .SingleOrDefault(token => token.IsKind(SyntaxKind.IdentifierToken)).Value; var diagnostic = Diagnostic.Create(Rule, fieldDeclaration.GetLocation(), name, accessToken.Value); context.ReportDiagnostic(diagnostic); } } } }
اولین کاری که در این کلاس انجام شده، خواندن سه رشتهی AnalyzerDescription (توضیحی در مورد آنالایزر)، AnalyzerMessageFormat (پیامی که به کاربر نمایش داده میشود) و AnalyzerTitle (عنوان پیام) از فایل Resources.resx است. این فایل را گشوده و محتوای آنرا مطابق تنظیمات ذیل تغییر دهید:
سپس کار به متد Initialize میرسد. در اینجا برخلاف مثالهای قسمتهای قبل، context مورد نیاز، توسط پارامترهای override شدهی کلاس پایه DiagnosticAnalyzer فراهم میشوند. برای مثال در متد Initialize، این فرصت را خواهیم داشت تا به ویژوال استودیو اعلام کنیم، قصد آنالیز فیلدها یا FieldDeclaration را داریم. پارامتر اول متد RegisterSyntaxNodeAction یک delegate یا Action است. این Action کار فراهم آوردن context کاری را برعهده دارد که نحوهی استفادهی از آنرا در متد analyzeFieldDeclaration میتوانید ملاحظه کنید.
سپس در اینجا نوع نود در حال آنالیز (همان نودی که کاربر در ویژوال استودیو انتخاب کردهاست یا در حال کار با آن است)، به نوع تعریف فیلد تبدیل میشود. سپس توکنهای آن استخراج شده و بررسی میشود که آیا یکی از این توکنها کلمهی کلیدی public هست یا خیر؟ اگر این فیلد عمومی تعریف شده بود، نام آنرا یافته و به عنوان یک Diagnostic جدید بازگشت و گزارش میدهیم.
ایجاد اولین Code fixer
در ادامه فایل CodeFixProvider.cs پیش فرض را گشوده و تغییرات ذیل را به آن اعمال کنید. در اینجا مهمترین تغییر صورت گرفته نسبت به قالب پیش فرض، اضافه شدن متد makePrivateDeclarationAsync بجای متد MakeUppercaseAsync از پیش موجود آن است:
using System.Collections.Immutable; using System.Composition; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace CodingStandards { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(CodingStandardsCodeFixProvider)), Shared] public class CodingStandardsCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds { get { return ImmutableArray.Create(CodingStandardsAnalyzer.DiagnosticId); } } public sealed override FixAllProvider GetFixAllProvider() { return WellKnownFixAllProviders.BatchFixer; } public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); // TODO: Replace the following code with your own analysis, generating a CodeAction for each fix to suggest var diagnostic = context.Diagnostics.First(); var diagnosticSpan = diagnostic.Location.SourceSpan; // Find the type declaration identified by the diagnostic. var declaration = root.FindToken(diagnosticSpan.Start) .Parent.AncestorsAndSelf().OfType<FieldDeclarationSyntax>() .First(); // Register a code action that will invoke the fix. context.RegisterCodeFix( CodeAction.Create("Make Private", c => makePrivateDeclarationAsync(context.Document, declaration, c)), diagnostic); } async Task<Document> makePrivateDeclarationAsync(Document document, FieldDeclarationSyntax declaration, CancellationToken c) { var accessToken = declaration.ChildTokens() .SingleOrDefault(token => token.Kind() == SyntaxKind.PublicKeyword); var privateAccessToken = SyntaxFactory.Token(SyntaxKind.PrivateKeyword); var root = await document.GetSyntaxRootAsync(c); var newRoot = root.ReplaceToken(accessToken, privateAccessToken); return document.WithSyntaxRoot(newRoot); } } }
کاری که در متد RegisterCodeFixesAsync انجام میشود، مشخص کردن اولین مکانی است که مشکلی در آن گزارش شدهاست. سپس به این مکان منوی Make Private با متد متناظر با آن معرفی میشود. در این متد، اولین توکن public، مشخص شده و سپس با یک توکن private جایگزین میشود. اکنون این syntax tree بازنویسی شده بازگشت داده میشود. با Syntax Factory در قسمت سوم آشنا شدیم.
خوب، تا اینجا یک analyzer و یک code fixer را تهیه کردهایم. برای آزمایش آن دکمهی F5 را فشار دهید تا وهلهای جدید از ویژوال استودیو که این آنالایزر جدید در آن نصب شدهاست، آغاز شود. البته باید دقت داشت که در اینجا باید پروژهی CodingStandards.Vsix را به عنوان پروژهی آغازین ویژوال استودیو معرفی کنید؛ چون پروژهی class library آنالایزرها را نمیتوان مستقیما اجرا کرد. همچنین یکبار کل solution را نیز build کنید.
پس از اینکه وهلهی جدید ویژوال استودیو شروع به کار کرد (بار اول اجرای آن کمی زمانبر است؛ زیرا باید تنظیمات وهلهی ویژهی اجرای افزونهها را از ابتدا اعمال کند)، همان پروژهی Student ابتدای بحث را در آن باز کنید.
نتیجهی اعمال این افزونهی جدید را در تصویر فوق ملاحظه میکنید. زیر سطرهای دارای فیلد عمومی، خط قرمز کشیده شدهاست (به علت تعریف DiagnosticSeverity.Error). همچنین حالت فعلی و حالت برطرف شده را نیز با رنگهای قرمز و سبز میتوان مشاهده کرد. کلیک بر روی گزینهی make private، سبب اصلاح خودکار آن سطر میگردد.
روش دوم آزمایش یک Roslyn Analyzer
همانطور که از انتهای بحث قسمت دوم بهخاطر دارید، این آنالایزرها را میتوان به کامپایلر نیز معرفی کرد. روش انجام اینکار در ویژوال استودیوی 2015 در تصویر ذیل نمایش داده شدهاست.
نود references را باز کرده و سپس بر روی گزینهی analyzers کلیک راست نمائید. در اینجا گزینهی Add analyzer را انتخاب کنید. در صفحهی باز شده بر روی دکمهی browse کلیک کنید. در اینجا میتوان فایل اسمبلی موجود در پوشهی CodingStandards\bin\Debug را به آن معرفی کرد.
بلافاصله پس از معرفی این اسمبلی، آنالایزر آن شناسایی شده و همچنین فعال میگردد.
در این حالت اگر برنامه را کامپایل کنیم، با خطاهای جدید فوق متوقف خواهیم شد و برنامه کامپایل نمیشود (به علت تعریف DiagnosticSeverity.Error).
Starting in .NET 5.0, Roslyn analyzers are included with the .NET SDK. Roslyn analyzers are enabled, by default, for projects that target .NET 5.0 or later. You can enable them on projects that target earlier .NET versions by setting the EnableNETAnalyzers property to true.
معرفی DNTProfiler
<runtime> <loadFromRemoteSources enabled="true"/> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-2.2.29.0" newVersion="2.2.29.0" /> </dependentAssembly> <dependentAssembly> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" /> <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" /> </dependentAssembly> </assemblyBinding> </runtime>