Babel Obfuscator یک ابزار خط فرمان سورس باز code obfuscation اسمبلیهای دات نت فریم ورک است.
این ابزار موارد زیر را پشتیبانی میکند:
- Obfuscate Namespace, Type (aslo generic types), Method, Events, Properties and Fields
- Unicode Normalization
- Support Generic Types and Virtual Function Obfuscation
- MSIL Control Flow Obfuscation
- String Encryption
- Dead Code Removal
- Selective Obfuscation with XML Rule Files
- Declarative Obfuscation using Custom Attributes
- MSBuild Integration
- Strong Name Signature
- Break tools like Reflector-Reflexil plug-in v0.8 and Ildasm
وبلاگ نویسنده آن
دریافت Babel Obfuscator از گوگل کد و یا رپید شیر
پس از نصب، جهت مشاهده پارامترهای خط فرمان آن به فایل ReadMe.htm مراجعه نمائید و یا اگر علاقمند باشید که از آن به صورت یکپارچه با Reflector استفاده کنید، میتوان از افزونه زیر کمک گرفت:
EF Code First #5
در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیتهای ردیابی تغییرات را از دست خواهیم داد و این عملیات، صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدلها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا میگردد.
هدف قرار دادن مرحلهای خاص یا لغو آن
به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:
Unable to update database to match the current model because there are pending changes and
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.
EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن میتوان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:
Add-Migration AddSomeProp2ToProject
به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}
public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}
مدلهای برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشدهاند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده میگردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .
در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:
Update-Database -Verbose
پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]
اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:
Add-Migration AddSomeProp3ToProject
Update-Database -Verbose
اینبار نیز یک کلاس جدید به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امکان به روز رسانی بانک اطلاعاتی میسر میگردد.
در ادامه برای مثال به این نتیجه رسیدهایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کردهایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه میشوند، میتوان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.
نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:
get-help update-database -detailed
به عنوان نمونه اگر در حین فراخوانی دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عملیات متوقف میشود. برای وادار کردن پروسه به انجام تغییرات بر روی بانک اطلاعاتی میتوان از پارامتر Force در اینجا استفاده کرد.
در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:
Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose
اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]
همانطور که ملاحظه میکنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمیشود و فرض بر این است که پیشتر اینکار را انجام دادهاید.
سفارشی سازی کلاسهای مهاجرت
تمام کلاسهای خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری میکنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاسهای مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، میتوان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کردهایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}
public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}
متد String در اینجا چنین امضایی دارد:
public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null,
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)
که برای نمونه در اینجا پارامتر defaultValue آنرا در کلاس AddSomeProp3ToProject مقدار دهی کردهایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:
ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'
تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شدهاست نیز میتواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونهای از آنرا در متد Up فوق مشاهده میکنید.
افزودن رکوردهای پیش فرض در حین به روز رسانی بانک اطلاعاتی
در قسمتهای قبل با متد Seed که به همراه آغاز کنندههای بانک اطلاعاتی EF ارائه شدهاند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration، چنین امری میسر است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}
متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی میکند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آنرا اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی میکند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص میشود که بر اساس چه خاصیتی باید در مورد update یا add تصمیمگیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object میتوان کمک گرفت new { p.Name, p.LastName } .
تولید اسکریپت به روز رسانی بانک اطلاعاتی
بهترین کار و امنترین روش حین انجام این نوع به روز رسانیها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس میتوان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداولتری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script
پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:
ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')
همانطور که ملاحظه میکنید، در یک مرحله، جدول پروژهها را به روز خواهد کرد و در مرحله بعد، سابقه آنرا در جدول __MigrationHistory ثبت میکند.
یک نکته:
اگر دستور فوق را بر روی برنامهای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور میتوان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:
Update-Database -Verbose -Script -SourceMigration:"stepName"
استفاده از DB Migrations در عمل
البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations، فایلهای اولیه DB Migration را ایجاد کنید. پیش فرضهای آن را نیز تغییر ندهید.
ج) هر بار که کلاسهای مدل برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script
به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید میگردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).
using System.Runtime.Serialization; namespace AOP03.DataContracts { [DataContract] public class User { [DataMember] public int Id { set; get; } [DataMember] public string Name { set; get; } } }
تهیه DataContractAspect جهت اعمال خودکار ویژگیهای DataContract و DataMember
using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.Serialization; using PostSharp.Aspects; using PostSharp.Extensibility; using PostSharp.Reflection; namespace AOP03 { [Serializable] //این ویژگی تنها نیاز است به کلاسها اعمال شود [MulticastAttributeUsage(MulticastTargets.Class)] public class DataContractAspect : TypeLevelAspect, IAspectProvider { public IEnumerable<AspectInstance> ProvideAspects(object targetElement) { var targetType = (Type)targetElement; //همان نوعی است که ویژگی جاری به آن اعمال خواهد شد //این سطر معادل است با درخواست تولید ویژگی دیتاکانترکت var introduceDataContractAspect = new CustomAttributeIntroductionAspect( new ObjectConstruction(typeof(DataContractAttribute).GetConstructor(Type.EmptyTypes))); //این سطر معادل است با درخواست تولید ویژگی دیتاممبر var introduceDataMemberAspect = new CustomAttributeIntroductionAspect( new ObjectConstruction(typeof(DataMemberAttribute).GetConstructor(Type.EmptyTypes))); //در اینجا کار اعمال ویژگی دیتاکانترکت به کلاسی که به عنوان پارامتر متد جاری //دریافت شده انجام خواهد شد yield return new AspectInstance(targetType, introduceDataContractAspect); //مرحله بعد کار اعمال ویژگی دیتاممبر به خواص کلاس است foreach (var property in targetType.GetProperties(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance)) { if (property.CanWrite) yield return new AspectInstance(property, introduceDataMemberAspect); } } } }
برای اعمال آن به سراسر برنامه تنها کافی است به فایل AssemblyInfo.cs پروژه مراجعه و سپس سطر زیر را به آن اضافه کنیم:
[assembly: DataContractAspect(AttributeTargetTypes = "AOP03.DataContracts.*")]
در این حالت اگر کلیه ویژگیهای کلاس User فوق را حذف و برنامه را کامپایل کنیم، با مراجعه به برنامه ILSpy میتوان صحت اعمال ویژگیها را به کمک PostSharp بررسی کرد:
طراحی رابط کاربری - راهنمایی فنی
بله. اگر حذف نشود، تمام اعمال را باز هم لاگ میکند که روی کارآیی تاثیر منفی خواهد گذاشت.
اگر در حال تهیه یک سایت چند زبانه هستید و همچنین سری مقالات Globalization در ASP.NET MVC رو دنبال کرده باشید میدانید که با تغییر Culture فایلهای Resource مورد نظر بارگذاری و نوشتههای سایت تغییر میابند ولی با تغییر Culture رفتار اعتبارسنجی در سمت سرور نیز تغییر و اعتبارسنجی بر اساس Culture فعلی سایت انجام میگیرد. بررسی این موضوع را با یک مثال شروع میکنیم.
یک پروژه وب بسازید سپس به پوشه Models یک کلاس با نام ValueModel اضافه کنید. تعریف کلاس به شکل زیر هست:
public class ValueModel { [Required] [Display(Name = "Decimal Value")] public decimal DecimalValue { get; set; } [Required] [Display(Name = "Double Value")] public double DoubleValue { get; set; } [Required] [Display(Name = "Integer Value")] public int IntegerValue { get; set; } [Required] [Display(Name = "Date Value")] public DateTime DateValue { get; set; } }
به سراغ کلاس HomeController بروید و کدهای زیر را اضافه کنید:
[HttpPost] public ActionResult Index(ValueModel valueModel) { if (ModelState.IsValid) { return Redirect("Index"); } return View(valueModel); }
Culture را به fa-IR تغییر میدهیم، برای اینکار در فایل web.config در بخش system.web کد زیر اضافه نمایید:
<globalization culture="fa-IR" uiCulture="fa-IR" />
و در نهایت به سراغ فایل Index.cshtml بروید کدهای زیر رو اضافه کنید:
@using (Html.BeginForm()) { <ol> <li> @Html.LabelFor(m => m.DecimalValue) @Html.TextBoxFor(m => m.DecimalValue) @Html.ValidationMessageFor(m => m.DecimalValue) </li> <li> @Html.LabelFor(m => m.DoubleValue) @Html.TextBoxFor(m => m.DoubleValue) @Html.ValidationMessageFor(m => m.DoubleValue) </li> <li> @Html.LabelFor(m => m.IntegerValue) @Html.TextBoxFor(m => m.IntegerValue) @Html.ValidationMessageFor(m => m.IntegerValue) </li> <li> @Html.LabelFor(m => m.DateValue) @Html.TextBoxFor(m => m.DateValue) @Html.ValidationMessageFor(m => m.DateValue) </li> <li> <input type="submit" value="Submit"/> </li> </ol> }
پرژه را اجرا نمایید و در ٢ تکست باکس اول ٢ عدد اعشاری را و در ٢ تکست باکس آخر یک عدد صحیح و یک تاریخ وارد نمایید و سپس دکمه Submit را بزنید. پس از بازگشت صفحه از سمت سرور در در ٢ تکست باکس اول با این پیامها روبرو میشوید که مقادیر وارد شده نامعتبر میباشند.
اگر پروژه رو در حالت دیباگ اجرا کنیم و نگاهی به داخل ModelState بیاندازیم، میبینیم که کاراکتر جدا کننده قسمت اعشاری برای fa-IR '/' میباشد که در اینجا برای اعداد مورد نظر کاراکتر '.' وارد شده است.
برای فایق شدن بر این مشکل یا باید سمت سرور اقدام کرد یا در سمت کلاینت. در بخش اول راه حل سمت کلاینت را بررسی مینماییم.
در سمت کلاینت برای اینکه کاربر را مجبور به وارد کردن کاراکترهای مربوط به Culture فعلی سایت نماییم باید مقادیر وارد شده را اعتبارسنجی و در صورت معتبر نبودن مقادیر پیام مناسب نشان داده شود. برای اینکار از کتابخانه jQuery Globalize استفاده میکنیم. برای اضافه کردن jQuery Globalize از طریق کنسول nuget فرمان زیر اجرا نمایید:
PM> Install-Package jquery-globalize
پس از نصب کتابخانه اگر به پوشه Scripts نگاهی بیاندازید میبینید که پوشەای با نام jquery.globalize اضافه شده است. درداخل پوشه زیر پوشەی دیگری با نام cultures وجود دارد که در آن Cultureهای مختلف وجود دارد و بسته به نیاز میتوان از آنها استفاده کرد. دوباره به سراغ فایل Index.cshtm بروید و فایلهای جاوا اسکریپتی زیر را به صفحه اضافه کنید:
<script src="~/Scripts/jquery.validate.js"> </script> <script src="~/Scripts/jquery.validate.unobtrusive.js"> </script> <script src="~/Scripts/jquery.globalize/globalize.js"> </script> <script src="~/Scripts/jquery.globalize/cultures/globalize.culture.fa-IR.js"> </script>
در فایل globalize.culture.fa-IR.js کاراکتر جدا کننده اعشاری '.' در نظر گرفته شده است که مجبور به تغییر آن هسیتم. برای اینکار فایل را باز کرده و numberFormat را پیدا کنید و آن را به شکل زیر تغییر دهید:
numberFormat: { pattern: ["n-"], ".": "/", currency: { pattern: ["$n-", "$ n"], ".": "/", symbol: "ریال" } },
و در نهایت کدهای زیر را به فایل Index.cshtml اضافه کنید و برنامه را دوباره اجرا نمایید:
Globalize.culture('fa-IR'); $.validator.methods.number = function(value, element) { if (value.indexOf('.') > 0) { return false; } var splitedValue = value.split('/'); if (splitedValue.length === 1) { return !isNaN(Globalize.parseInt(value)); } else if (splitedValue.length === 2 && $.trim(splitedValue[1]).length === 0) { return false; } return !isNaN(Globalize.parseFloat(value)); }; };
در خط اول Culture را ست مینمایم و در ادامه نحوه اعتبارسنجی را در unobtrusive validation تغییر میدهیم. از آنجایی که برای اعتبارسنجی عدد وارد شده از تابع parseFloat استفاده میشود، کاراکتر جدا کننده قسمت اعشاری قابل قبول برای این تابع '.' است پس در داخل تابع دوباره '/' به '.' تبدیل میشود و سپس اعتبارسنجی انجام میشود از اینرو اگر کاربر '.' را نیز وارد نماید قابل قبول است به همین دلیل با این خط کد if (value.indexOf('.') > 0) وجود نقطه را بررسی میکنیم تا در صورت وجود '.' پیغام خطا نشان داده شود.در خط بعدی بررسی مینماییم که اگر عدد وارد شده اعشاری نباشد از تابع parseInt استفاده نماییم. در خط بعدی این حالت را بررسی مینماییم که اگر کاربر عددی همچون /١٢ وارد کرد پیغام خطا صادر شود.
برای اعتبارسنجی تاریخ شمسی متاسفانه توابع کمکی برای تبدیل تاریخ در فایل globalize.culture.fa-IR.js وجود ندارد ولی اگر نگاهی به فایلهای Culture عربی بیاندازید همه دارای توابع کمکی برای تبدیل تاریج هجری به میلادی هستند به همین دلیل امکان اعتبارسنجی تاریخ شمسی با استفاده از jQuery Globalize میسر نمیباشد. من خودم تعدادی توابع کمکی را به globalize.culture.fa-IR.js اضافه کردەام که از تقویم فارسی آقای علی فرهادی برداشت شده است و با آنها کار اعتبارسنجی را انجام میدهیم. لازم به ذکر است این روش ١٠٠% تست نشده است و شاید راه کاملا اصولی نباشد ولی به هر حال در اینجا توضیح میدهم. در فایل globalize.culture.fa-IR.js قسمت Gregorian_Localized را پیدا کنید و آن را با کدهای زیر جایگزین کنید:
Gregorian_Localized: { firstDay: 6, days: { names: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesAbbr: ["یکشنبه", "دوشنبه", "سه شنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه"], namesShort: ["ی", "د", "س", "چ", "پ", "ج", "ش"] }, months: { names: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""], namesAbbr: ["ژانویه", "فوریه", "مارس", "آوریل", "می", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اُکتبر", "نوامبر", "دسامبر", ""] }, AM: ["ق.ظ", "ق.ظ", "ق.ظ"], PM: ["ب.ظ", "ب.ظ", "ب.ظ"], patterns: { d: "yyyy/MM/dd", D: "yyyy/MM/dd", t: "hh:mm tt", T: "hh:mm:ss tt", f: "yyyy/MM/dd hh:mm tt", F: "yyyy/MM/dd hh:mm:ss tt", M: "dd MMMM" }, JalaliDate: { g_days_in_month: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], j_days_in_month: [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29] }, gregorianToJalali: function (gY, gM, gD) { gY = parseInt(gY); gM = parseInt(gM); gD = parseInt(gD); var gy = gY - 1600; var gm = gM - 1; var gd = gD - 1; var gDayNo = 365 * gy + parseInt((gy + 3) / 4) - parseInt((gy + 99) / 100) + parseInt((gy + 399) / 400); for (var i = 0; i < gm; ++i) gDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i]; if (gm > 1 && ((gy % 4 == 0 && gy % 100 != 0) || (gy % 400 == 0))) /* leap and after Feb */ ++gDayNo; gDayNo += gd; var jDayNo = gDayNo - 79; var jNp = parseInt(jDayNo / 12053); jDayNo %= 12053; var jy = 979 + 33 * jNp + 4 * parseInt(jDayNo / 1461); jDayNo %= 1461; if (jDayNo >= 366) { jy += parseInt((jDayNo - 1) / 365); jDayNo = (jDayNo - 1) % 365; } for (var i = 0; i < 11 && jDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; ++i) { jDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; } var jm = i + 1; var jd = jDayNo + 1; return [jy, jm, jd]; }, jalaliToGregorian: function (jY, jM, jD) { jY = parseInt(jY); jM = parseInt(jM); jD = parseInt(jD); var jy = jY - 979; var jm = jM - 1; var jd = jD - 1; var jDayNo = 365 * jy + parseInt(jy / 33) * 8 + parseInt((jy % 33 + 3) / 4); for (var i = 0; i < jm; ++i) jDayNo += Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[i]; jDayNo += jd; var gDayNo = jDayNo + 79; var gy = 1600 + 400 * parseInt(gDayNo / 146097); /* 146097 = 365*400 + 400/4 - 400/100 + 400/400 */ gDayNo = gDayNo % 146097; var leap = true; if (gDayNo >= 36525) /* 36525 = 365*100 + 100/4 */ { gDayNo--; gy += 100 * parseInt(gDayNo / 36524); /* 36524 = 365*100 + 100/4 - 100/100 */ gDayNo = gDayNo % 36524; if (gDayNo >= 365) gDayNo++; else leap = false; } gy += 4 * parseInt(gDayNo / 1461); /* 1461 = 365*4 + 4/4 */ gDayNo %= 1461; if (gDayNo >= 366) { leap = false; gDayNo--; gy += parseInt(gDayNo / 365); gDayNo = gDayNo % 365; } for (var i = 0; gDayNo >= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap) ; i++) gDayNo -= Globalize.culture().calendars.Gregorian_Localized.JalaliDate.g_days_in_month[i] + (i == 1 && leap); var gm = i + 1; var gd = gDayNo + 1; return [gy, gm, gd]; }, checkDate: function (jY, jM, jD) { return !(jY < 0 || jY > 32767 || jM < 1 || jM > 12 || jD < 1 || jD > (Globalize.culture().calendars.Gregorian_Localized.JalaliDate.j_days_in_month[jM - 1] + (jM == 12 && !((jY - 979) % 33 % 4)))); }, convert: function (value, format) { var day, month, year; var formatParts = format.split('/'); var dateParts = value.split('/'); if (formatParts.length !== 3 || dateParts.length !== 3) { return false; } for (var j = 0; j < formatParts.length; j++) { var currentFormat = formatParts[j]; var currentDate = dateParts[j]; switch (currentFormat) { case 'dd': if (currentDate.length === 2 || currentDate.length === 1) { day = currentDate; } else { year = currentDate; } break; case 'MM': month = currentDate; break; case 'yyyy': if (currentDate.length === 4) { year = currentDate; } else { day = currentDate; } break; default: return false; } } year = parseInt(year); month = parseInt(month); day = parseInt(day); var isValidDate = Globalize.culture().calendars.Gregorian_Localized.checkDate(year, month, day); if (!isValidDate) { return false; } var grDate = Globalize.culture().calendars.Gregorian_Localized.jalaliToGregorian(year, month, day); var shDate = Globalize.culture().calendars.Gregorian_Localized.gregorianToJalali(grDate[0], grDate[1], grDate[2]); if (year === shDate[0] && month === shDate[1] && day === shDate[2]) { return true; } return false; } },
روال کار در تابع convert به اینصورت است که ابتدا تاریخ وارد شده را بررسی مینماید تا معتبر بودن آن معلوم شود به عنوان مثال اگر تاریخی مثل 1392/12/31 وارد شده باشد و در ادامه برای بررسی بیشتر تاریخ یک بار به میلادی و تاریخ میلادی دوباره به شمسی تبدیل میشود و با تاریخ وارد شده مقایسه میشود و در صورت برابری تاریخ معتبر اعلام میشود. در فایل Index.cshtml کدهای زیر اضافی نمایید:
$.validator.methods.date = function (value, element) { return Globalize.culture().calendars.Gregorian_Localized.convert(value, 'yyyy/MM/dd'); };
برای اعتبارسنجی تاریخ میتوانید از ٢ فرمت استفاده کنید:
١ – yyyy/MM/dd
٢ – dd/MM/yyyy
البته از توابع اعتبارسنجی تاریخ میتوانید به صورت جدا استفاده نمایید و لزومی ندارد آنها را همراه با jQuery Globalize بکار ببرید. در آخر خروجی کار به این شکل است:
در کل استفاده از jQuery Globalize برای اعتبارسنجی در سایتهای چند زبانه به نسبت خوب میباشد و برای هر زبان میتوانید از culture مورد نظر استفاده نمایید. در قسمت دوم این مطلب به بررسی بخش سمت سرور میپردازیم.
همچنین یک Interface را نیز به نام IResourceManager تعریف میکنیم:
public interface IResourceManager { IStringLocalizer Localizer { get; } }
سپس دو کلاس دیگر را ایجاد کرده و آنها را از این اینترفیس مشتق میکنیم. در این اینترفیس، یک خصوصیت Localizer از نوع IStringLocalizer وجود دارد که باید در این کلاسها پیاده سازی شود. این کار را به کمک IStringLocalizerFactory انجام داده و مشخص میکنیم هر کلاس، از کدام ریسورس استفاده کند:
public class SchoolResourceManager : IResourceManager { public SchoolResourceManager (IStringLocalizerFactory factory) { Localizer = factory.Create(typeof(SchoolResource)); } public IStringLocalizer Localizer { get; } }
public class ArtSchoolResourceManager : IResourceManager { public ArtSchoolResourceManager(IStringLocalizerFactory factory) { Localizer = factory.Create(typeof(ArtSchoolResource)); } public IStringLocalizer Localizer { get; } }
سپس در فایل appsetting.json پروژه، یک کلید را تعریف میکنیم تا مشخص کند، نوع application، از نوع مدرسه هست یا از نوع هنرستان:
{ "ConnectionStrings": { "str": "..." }, "Type": "ArtSchool" }
در متد ConfigureServices در فایل startup تعیین میکنیم هنگام شروع به کار برنامه، با توجه به مقدار درون appsetting، کدام سرویس (manager) تزریق شود:
public void ConfigureServices(IServiceCollection services) { services.AddLocalization(); services.AddSingleton<SchoolResourceManager>(); services.AddSingleton<ArtSchoolResourceManager>(); services.AddSingleton<IResourceManager>(cnf => { var type = Configuration.GetSection("Type").Value; if (type == "ArtSchool") return cnf.GetServices<ArtSchoolResourceManager>().FirstOrDefault(); return cnf.GetServices<SchoolResourceManager>().FirstOrDefault(); }); }
خوب حالا با خیال راحت برنامه را توسعه میدهیم و در viewها و Controllerها از همان کلیدهای School و Student استفاده میکنیم:
: View
@inject IResourceManager ResourceManager <h2>@ResourceManager.Localizer["Student"] : </h2>
:Controller
public class TestController : Controller { private readonly IResourceManager _resourceManager; public TestController (IResourceManager resourceManager) { _resourceManager = resourceManager; } }
چقدر خوب میشد اگر IDE ویژوال استودیو دات نت اشیایی از نوع disposable را برای ما مشخص میکرد و در ادامه پیشنهاد استفاده از عبارت using را برای آنها میداد؟
سؤال: این مساله چه اهمیتی دارد؟
به مثال متداول زیر دقت بفرمائید:
string connString = "...";
SqlConnection conn = new SqlConnection(connString);
string cmdString = "select * from author where name=N'test'";
SqlCommand cmd = new SqlCommand(cmdString, conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
while(reader.Read())
{
...
}
conn.Close();
Exception Details: System.InvalidOperationException: Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use and max pool size was reached.
using (SqlConnection connection =
new SqlConnection(connectionString))
{
...
Agent Johnson در حقیقت پلاگینی است برای پلاگین دیگری به نام Resharper . این پلاگین را از آدرس زیر میتوان دریافت کرد (احتمالا به یک پروکسی نیاز خواهد بود ...):
http://code.google.com/p/agentjohnsonplugin/
البته بحث تشخیص اشیاء disposable یکی از تواناییهای آن است. سایر موارد را در آدرس فوق میتوانید مشاهده نمائید.
- ViewUsers
- CreateUser
- EditUser
- DeleteUser
همانطور که مشاهده میکنید، المنتهایی در صفحه وجود دارند که کاربر X نباید آنها را مشاهده کند. از جمله دکمه حذف کاربر و دکمه ایجاد کاربر. برای مخفی کردن آنها چه راهحلی را میتوان ارائه داد؟ شاید بخواهید برای اینکار از ngIf* استفاده کنید. برای اینکار کافی است دست بکار شوید تا مشکلاتی را که در این روش به آنها بر میخورید، متوجه شوید. از جمله این مشکلات میتوان به پیچیدگی بسیار زیاد و وجود کدهای تکراری در هر کامپوننت اشاره کرد (در بهترین حالت هر کامپوننت باید سرویس حاوی دسترسیها را در خود تزریق کرده و با استفاده از توابعی، وجود یا عدم وجود دسترسی را بررسی کند).
راهحل بهتر، استفاده از یک Directive سفارشی است. همچنین ماژول Ng2Permission علاوه بر فراهم کردن Directive جهت مدیریت المنتهای روی صفحه، امکاناتی را جهت نگهداری و تعریف دسترسیهای جدید و همچنین محافظت از Routeها فراهم کرده است. این ماژول الهام گرفته از ماژول angular-permission میباشد.
کافی است با استفاده از دستور زیر این ماژول را نصب کنید:
npm install angular2-permission --save
بعد از نصب، ماژول Ng2Permission را در قسمت imports در ماژول اصلی برنامه، اضافه کنید.
import { Ng2Permission } from 'angular2-permission'; @NgModule({ imports: [ Ng2Permission ] })
مدیریت دسترسیها
توضیحات | امضاء |
تعریف دسترسیهای جدید | define(permissions: Array<string>): void |
افزودن دسترسی جدید | add(permission: string ) : void |
حذف دسترسی مشخص شده | remove(permission: string ) : void |
برسی اینکه دسترسی قبلا تعریف شده است؟ | hasDefined( permission : string ) : boolean |
برسی اینکه حداقل یکی از دسترسیهای ورودی قبلا تعریف شده است؟ | hasOneDefined(permissions: Array < string > ) : boolean |
حذف تمامی دسترسیها | clearStore( ) : void |
دریافت تمامی دسترسیهای تعریف شده | get store ( ) : Array < string> |
Emitter جهت تغییر در دسترسیها | get permissionStoreChangeEmitter ( ) : EventEmitter< an y> |
برای مثال جهت تعریف دسترسیهای جدید، کافی است سرویس PermissionService را تزریق کرده و با استفاده از متدهای define اقدام به اینکار کنید:
import { PermissionService } from 'angular2-permission'; @Component({ […] }) export class LoginComponent implements OnInit { constructor(private _permissionService: PermissionService) { this._permissionService.define(['ViewUsers', 'CreateUser', 'EditUser', 'DeleteUser']); } }
آنچه که واضح است، این است که لیست دسترسیها میتوانند از سمت سرور تامین شوند.
محافظت از مسیرهای تعریف شده
import { PermissionGuard, IPermissionGuardModel } from 'angular2-permission'; […] const routes: Routes = [ { path: 'login', component: LoginComponent, children: [] }, { path: 'users', component: UserListComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['ViewUsers'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: '403', component: AccessDeniedComponent, children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
توضیحات | خصوصیت |
فقط دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Only |
تمامی دسترسیهای تعریف شده، به جز دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Except |
در صورتیکه تقاضای غیر مجازی جهت پیمایش به این مسیر صادر شد، کاربر را به این مسیر هدایت میکند. | RedirectTo |
{ path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['Admin', 'CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }
محافظت از المنتهای صفحه
توضیحات | Input type | Directive |
فقط دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Arrayy<string> | hasPermission |
تمامی دسترسیهای موجود، به جز دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Array<string> | exceptPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت دارد. | string | Function | onAuthorizedPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت را ندارد. | string | Function | onUnauthorizedPermission |
<button type="button" [hasPermission]="['DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
در صورتیکه بیش از یک دسترسی مد نظر باشد، با کاما از هم جدا خواهند شد.
<button type="button" [hasPermission]="[ 'Admin', 'DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [exceptPermission]="['GeustUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [hasPermission]="['GeustUser']" onAuthorizedPermission="enable" onUnauthorizedPermission="disable"> <span aria-hidden="true"></span> Delete </button>
رفتار | مقدار |
حذف خصوصیت disabled از المنت | enable |
افزودن خصوصیت disabled به المنت | disable |
تنظیم استایل display به inherit | show |
تنظیم استایل display به none | hide |
@Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent { constructor() { } OnAuthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility ="inherit"; } OnUnauthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility = "hidden"; } }
<button [hasPermission]="['CreateUser']" [onAuthorizedPermission]="OnAuthorizedPermission" [onUnauthorizedPermission]="OnUnauthorizedPermission"> <span aria-hidden="true"></span> Add New User </button>
یکی از مواردی که ممکن است در محیط کاری با آن برخورد داشت، تقاضای تولید فایل word یک گزارش با فرمتی مشخص از یک برنامه ASP.Net است. برای مثال یک قالب درست کردهاند که header و footer و کلا یک فرمت رسمی دارد. الان برنامه شما باید این فایل word رسمی را با گزارشی که تولید میکند پر کند. حالا اینجاست که گرفتاری برنامه نویس شروع میشود! روی سرور باید word نصب باشد تا توسط اشیاء COM آن بتوان یک چنین کارهایی را آنهم با ASP.Net که به صورت پیش فرض کمترین سطح دسترسی را روی سیستم دارد انجام داد. یا اینکه باید به سراغ کامپوننتهای تجاری رفت و حالا اینجا با این وضع تحریم و غیره چگونه بتوان آنها را خریداری کرد یا شاید احتمالا در سایتهای وارز بتوان نسخه تکه پاره شده آنها را یافت. مشکلی هم که این نوع کامپوننتها دارند این است که ممکن است سال دیگر اصلا ساپورت نشوند. محصولات مایکروسافت هم که مرتبا در حال به روز رسانی هستند. در این حالت برنامه متکی به این نوع کامپوننتهای تجاری سورس بسته در همان نگارش قبلی خود مجبور است باقی بماند.
خوشبختانه با ارائه آفیس 2007 و فرمت OpenXML فایلهای آن، این مشکل تقریبا مرتفع شده است. مایکروسافت نیز برای سهولت تولید این نوع اسناد، OpenXML SDK را ارائه داده است که از آدرس زیر قابل دریافت است:
Open XML Format SDK 1.0
البته پیش نمایش نگارش دو SDK آن نیز موجود است که در مطلب جاری به آن پرداخته نخواهد شد.
فایلهای office 2007 از یک فایلzip تشکیل شده از چند فایل xml داخل آن، ایجاد شدهاند. برای مثال یک فایل docx را با winrar یا امثال آن باز کنید (تصویر زیر):
برای کار با اینگونه اسناد باید با اصطلاحات زیر آشنا شد:
Package : فایل zip شما (همان فایل برای مثال docx) اینجا یک بسته نام دارد.
Parts : اجزای این بسته که همان فایلهای آن هستند، parts نامیده شده اند.
Relations : اگر به فایلهای موجود در یک بسته دقت کنید، فایلهایی با پسوند rels را خواهید دید که بیانگر نحوه ارتباط Parts با یکدیگر هستند.
Relations Ids: هر ارتباط با یک ID منحصربفرد تعریف میگردد.
اگر علاقمند باشید که پوستری را در این رابطه مشاهده نمائید میتوان به آدرس زیر مراجعه نمود.
Open XML Developer Map
نحوه استفاده از OpenXML SDK در دات نت:
ابتدا باید ارجاعی را به فایل DocumentFormat.OpenXml.dll که پس از نصب در مسیر OpenXMLSDK\1.0.1825\lib قرار گرفته است به پروژه افزود. سپس نیاز است تا ارجاعی به کتابخانه WindowsBase نیز به برنامه افزوده شود (تصویر زیر). افزودن ارجاعی به این کتابخانه جهت کامپایل برنامه ضروری است (شکل زیر).
تا اینجا ارجاعات برنامه به صورت زیر خواهند بود:
یک مثال ساده:
قصد داریم یک فایل docx ساده را با استفاده از OpenXML SDK ایجاد کنیم. در مثال زیر فرمت متغیر docXml را میتوان با ایجاد یک فایل docx ساده در word و سپس باز کردن بسته فشرده شده آن و مشاهده محتوای فایل word\document.xml بدست آورد.
using System.IO;
using System.Text;
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
namespace OpenXMLTestApp
{
class CWord
{
public static void CreateDocument(string documentFileName, string text)
{
using (WordprocessingDocument wordDoc =
WordprocessingDocument.Create(documentFileName, WordprocessingDocumentType.Document))
{
MainDocumentPart mainPart = wordDoc.AddMainDocumentPart();
string docXml =
@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>
<w:document xmlns:w=""http://schemas.openxmlformats.org/wordprocessingml/2006/main"">
<w:body><w:p><w:r><w:t>#REPLACE#</w:t></w:r></w:p></w:body>
</w:document>";
docXml = docXml.Replace("#REPLACE#", text);
using (Stream stream = mainPart.GetStream())
{
byte[] buf = (new UTF8Encoding()).GetBytes(docXml);
stream.Write(buf, 0, buf.Length);
}
}
}
}
}
CWord.CreateDocument("test.docx", "سلام دنیا");
برای مطالعه بیشتر میتوان به منابع زیر مراجعه نمود:
یک ویدیوی آموزشی رایگان از مایکروسافت
دریافت
سؤالات متداول در MSDN
http://msdn.microsoft.com/en-us/library/bb491088.aspx
البته اگر پس از نصب SDK به پوشه doc آن مراجعه نمائید، این سؤال و جوابها را در فایل راهنمای chm آن نیز میتوان پیدا کرد.
مثال دیگری در مورد ایجاد یک گزارش از بانک اطلاعاتی و گرفتن خروجی docx از آن
http://openxmldeveloper.org/articles/GenerateWordTable.aspx
البته این مثال خیلی قدیمی است و قسمتهای کار با پکیج را با SDK ارائه شده میتوان به صورت خودکار انجام داد. اما حداقل نحوه تولید جداول استاندارد OpenXML را میتوان از آن ایده گرفت.
مثالی در مورد نحوه قرار دادن عکس در فایل docx تولیدی
همچنین مثالهای بیشتری را در وبلاگهای مربوطه میتوان یافت:
http://blogs.msdn.com/brian_jones/
http://blogs.msdn.com/ericwhite/default.aspx