اشتراکها
اشتراکها
امکانات جدید GIT 2.7
طی این پست
با تزریق وابستگیها در Asp.net MVC آشنا شدید. روش ذکر شده در آن برای کنترلرهای Web Api جوابگو نیست و باید از روشهای دیگری
برای این منظور استفاده نماییم.
نکته 1: برای پیاده سازی این مثالها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است میتوانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: میتوانید از مقاله [هاست سرویسهای Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویسهای web Api خود استفاده نمایید.
نکته 1: برای پیاده سازی این مثالها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است میتوانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: میتوانید از مقاله [هاست سرویسهای Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویسهای web Api خود استفاده نمایید.
روش اول
اگر قبلا در این زمینه جستجو کرده باشید، به احتمال
زیاد با مفهوم IDependencyResolver بیگانه نیستید. درباره استفاده از این
روش مقالات متعددی نوشته شده است؛ حتی در مثالهای موجود در خود سایت MSDN
نیز این روش را مرسوم دانسته و آن را به اشتراک میگذارند. جهت نمونه
میتوانید این پروژه را دانلود کرده و کدهای آن را بررسی کنید.
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver میباشد؛ به صورت زیر:
اینترفیس IDependencyResolver از اینترفیس دیگری به نام
IDependencyScope ارث میبرد که دارای دو متد اصلی به نامهای GetService و
GetServices است که جهت وهله سازی کنترلرها استفاده میشوند. با فراخوانی
این متدها، نمونهی ساخته شده توسط Container بازگشت داده خواهد شد.
کاربرد متد BeginScope چیست؟
کنترلرها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخهی عمر کنترلرها و منابع در اختیار آنها، از متد BeginScope استفاده میشود. به این صورت که نمونهی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controllerها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
اگر از UnityContainer استفاده میکنید کافیست تکه کد زیر را جایگزین کلاس بالا نمایید:
برای جستجوی خودکار کنترلرها و رجیستر کردن آنها به برنامه
Windsor امکانات جالبی را در اختیار ما قرار میدهد. ابتدا یک Installer ایجاد میکنیم:
در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
»ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver نوشته شده را به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
نکته: این روش به دلیل استفاده از الگوی ServiceLocator و همچنین نداشتن Context درخواست ها روشی منسوخ شده میباشد که طی این مقاله جناب نصیری به صورت کامل به این مبحث پرداخته اند.
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver میباشد؛ به صورت زیر:
public class ApiDependencyResolver : IDependencyResolver { public ApiDependencyResolver(IWindsorContainer container) { Container = container; } public IWindsorContainer Container { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (Kernel.ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (Kernel.ComponentNotFoundException) { return Enumerable.Empty<object>(); } } public IDependencyScope BeginScope() { return new SharedDependencyResolver(Container); } public void Dispose() { Container.Dispose(); } }
کاربرد متد BeginScope چیست؟
کنترلرها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخهی عمر کنترلرها و منابع در اختیار آنها، از متد BeginScope استفاده میشود. به این صورت که نمونهی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controllerها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
public class SharedDependencyResolver : IDependencyScope { public SharedDependencyResolver(IWindsorContainer container) { Container = container; Scope = Container.BeginScope(); } public IWindsorContainer Container { get; private set; } public IDisposable Scope { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (ComponentNotFoundException) { return null; } } public void Dispose() { Scope.Dispose(); } }
public IDependencyScope BeginScope() { var child = container.CreateChildContainer(); return new ApiDependencyResolver(child); }
public class KernelInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Classes.FromThisAssembly().BasedOn<ApiController>().LifestyleTransient()); container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel, true)); } }
در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
»ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver نوشته شده را به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var container = new WindsorContainer(); container.Install(new KernelInstaller()); var config = new HttpConfiguration { DependencyResolver = new ApiDependencyResolver(container) }; config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "Default" , routeTemplate: "{controller}/{action}/{name}" , defaults: new { name = RouteParameter.Optional } ); config.EnsureInitialized(); appBuilder.UseWebApi( config ); } }
n general terms, caching takes place where the frequently-used data is stored, so that the application can quickly access the data rather than accessing the data from the source. Caching can improve the performance and scalability of the application dramatically and can help us to remove the unnecessary requests from the external data sources for the data that changes infrequently.
پس از ایجاد AuthService در قسمت قبل، اکنون میخواهیم از آن برای تکمیل صفحهی ورود به سیستم و همچنین تغییر منوی بالای برنامه یا همان کامپوننت header استفاده کنیم.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
پس از اجرای این دستورات، ابتدا به فایل app.module.ts مراجعه کرده و تعریف این ماژول را که به صورت خودکار به قسمت imports اضافه شدهاست، به قبل از AppRoutingModule منتقل میکنیم تا مسیریابیهای آن توسط catch all ماژول AppRouting لغو نشوند:
همچنین به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه میکنیم:
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
مدل login را همان اینترفیس Credentials تعریف شدهی در قسمت قبل درنظر گرفتهایم. به همراه دو خاصیت error جهت نمایش خطاها در ذیل قسمت لاگین و همچنین ذخیره سازی returnUrl در صورت وجود:
AuthService را به سازندهی کامپوننت لاگین تزریق کردهایم تا بتوان از متدهای login و Logout آن استفاده کرد. همچنین از سرویس Router برای استفادهی از متد navigate آن استفاده میکنیم و از سرویس ActivatedRoute برای دریافت کوئری استرینگ returnUrl، در صورت وجود، کمک خواهیم گرفت.
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
در اینجا توسط وهلهی تزریق شدهی this.authService، کار فراخوانی متد login و ارسال شیء Credentials به سمت سرور صورت میگیرد. خروجی متد login یک Observable از نوع boolean است. بنابراین در صورت true بودن این مقدار و یا موفقیت آمیز بودن عملیات لاگین، کاربر را به یکی از دو صفحهی protectedPage و یا this.returnUrl (در صورت وجود)، هدایت خواهیم کرد.
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
در اینجا خاصیتهای نام کاربری، کلمهی عبور و همچنین rememberMe مدل، از کاربر دریافت میشوند؛ به همراه بررسی اعتبارسنجی سمت کلاینت آنها؛ با این شکل:
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
این کامپوننت وضعیت گزارش شدهی ورود شخص را توسط خاصیت isLoggedIn در اختیار قالب خود قرار میدهد. برای این منظور به سرویس AuthService تزریق شدهی در سازندهی خود نیاز دارد.
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
Status بازگشت داده شدهی توسط آن از نوع boolean است و در صورت true بودن، خاصیت isLoggedIn را نیز true میکند که از آن در قالب این کامپوننت برای نمایش و یا مخفی کردن لینکهای login و logout استفاده خواهیم کرد.
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
در اینجا توسط ngIfها وضعیت خاصیت isLoggedIn این کامپوننت را بررسی میکنیم. اگر true باشد، Logoff به همراه نام نمایشی کاربر را در منوی راهبری سایت ظاهر خواهیم کرد و در غیراینصورت لینک به صفحهی Login را نمایش میدهیم.
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
در اینجا به کمک سرویس تزریقی AuthService، یکبار با استفاده از متد getDecodedAccessToken آن، مقدار اصلی توکن را و بار دیگر توسط متد getAccessTokenExpirationDate آن، تاریخ انقضای توکن دریافتی از سمت سرور را نمایش میدهیم؛ با این قالب:
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
ایجاد ماژول Dashboard و تعریف کامپوننت صفحهی محافظت شده
قصد داریم پس از لاگین موفق، کاربر را به یک صفحهی محافظت شده هدایت کنیم. به همین جهت ماژول جدید Dashboard را به همراه کامپوننت یاد شده، به برنامه اضافه میکنیم:
>ng g m Dashboard -m app.module --routing >ng g c Dashboard/ProtectedPage
import { DashboardModule } from "./dashboard/dashboard.module"; @NgModule({ imports: [ //... DashboardModule, AppRoutingModule ] }) export class AppModule { }
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent } ];
ایجاد صفحهی ورود به سیستم
در قسمت اول این سری، کارهای «ایجاد ماژول Authentication و تعریف کامپوننت لاگین» انجام شدند. اکنون میخواهیم کامپوننت خالی لاگین را به نحو ذیل تکمیل کنیم:
export class LoginComponent implements OnInit { model: Credentials = { username: "", password: "", rememberMe: false }; error = ""; returnUrl: string;
constructor( private authService: AuthService, private router: Router, private route: ActivatedRoute) { } ngOnInit() { // reset the login status this.authService.logout(false); // get the return url from route parameters this.returnUrl = this.route.snapshot.queryParams["returnUrl"]; }
اکنون متد ارسال این فرم چنین شکلی را پیدا میکند:
submitForm(form: NgForm) { this.error = ""; this.authService.login(this.model) .subscribe(isLoggedIn => { if (isLoggedIn) { if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); } } }, (error: HttpErrorResponse) => { console.log("Login error", error); if (error.status === 401) { this.error = "Invalid User name or Password. Please try again."; } else { this.error = `${error.statusText}: ${error.message}`; } }); }
صفحهی خالی protectedPage را در ابتدای بحث، در ذیل ماژول Dashboard ایجاد کردیم.
در سمت سرور هم در صورت شکست اعتبارسنجی کاربر، یک return Unauthorized صورت میگیرد که معادل error.status === 401 کدهای فوق است و در اینجا در قسمت خطای عملیات بررسی شدهاست.
قالب این کامپوننت نیز به صورت ذیل به model از نوع Credentials آن متصل شدهاست:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Login</h2> </div> <div class="panel-body"> <form #form="ngForm" (submit)="submitForm(form)" novalidate> <div class="form-group" [class.has-error]="username.invalid && username.touched"> <label for="username">User name</label> <input id="username" type="text" required name="username" [(ngModel)]="model.username" #username="ngModel" class="form-control" placeholder="User name"> <div *ngIf="username.invalid && username.touched"> <div class="alert alert-danger" *ngIf="username.errors['required']"> Name is required. </div> </div> </div> <div class="form-group" [class.has-error]="password.invalid && password.touched"> <label for="password">Password</label> <input id="password" type="password" required name="password" [(ngModel)]="model.password" #password="ngModel" class="form-control" placeholder="Password"> <div *ngIf="password.invalid && password.touched"> <div class="alert alert-danger" *ngIf="password.errors['required']"> Password is required. </div> </div> </div> <div class="checkbox"> <label> <input type="checkbox" name="rememberMe" [(ngModel)]="model.rememberMe"> Remember me </label> </div> <div class="form-group"> <button type="submit" class="btn btn-primary" [disabled]="form.invalid ">Login</button> </div> <div *ngIf="error" class="alert alert-danger " role="alert "> {{error}} </div> </form> </div> </div>
برای آزمایش برنامه، نام کاربری Vahid و کلمهی عبور 1234 را وارد کنید.
تکمیل کامپوننت Header برنامه
در ادامه، پس از لاگین موفق شخص، میخواهیم صفحهی protectedPage را نمایش دهیم:
در این صفحه، Login از منوی سایت حذف شدهاست و بجای آن Logout به همراه «نام نمایشی کاربر» ظاهر شدهاند. همچنین توکن decode شده به همراه تاریخ انقضای آن نمایش داده شدهاند.
برای پیاده سازی این موارد، ابتدا از کامپوننت Header شروع میکنیم:
export class HeaderComponent implements OnInit, OnDestroy { title = "Angular.Jwt.Core"; isLoggedIn: boolean; subscription: Subscription; displayName: string; constructor(private authService: AuthService) { }
اکنون در روال رخدادگردان ngOnInit، مشترک authStatus میشود که یک BehaviorSubject است و از آن جهت صدور رخدادهای authService به تمام کامپوننتهای مشترک به آن استفاده کردهایم:
ngOnInit() { this.subscription = this.authService.authStatus$.subscribe(status => { this.isLoggedIn = status; if (status) { this.displayName = this.authService.getDisplayName(); } }); }
همچنین اگر این وضعیت true باشد، مقدار DisplayName کاربر را نیز از سرویس authService دریافت کرده و توسط خاصیت this.displayName در اختیار قالب Header قرار میدهیم.
در آخر برای جلوگیری از نشتی حافظه، ضروری است اشتراک به authStatus، در روال رخدادگردان ngOnDestroy لغو شود:
ngOnDestroy() { // prevent memory leak when component is destroyed this.subscription.unsubscribe(); }
همچنین در قالب Header، مدیریت دکمهی Logout را نیز انجام خواهیم داد:
logout() { this.authService.logout(true); }
با این مقدمات، قالب Header اکنون به صورت ذیل تغییر میکند:
<nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <a class="navbar-brand" [routerLink]="['/']">{{title}}</a> </div> <ul class="nav navbar-nav"> <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }"> <a class="nav-link" [routerLink]="['/welcome']">Home</a> </li> <li *ngIf="!isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" queryParamsHandling="merge" [routerLink]="['/login']">Login</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" (click)="logout()">Logoff [{{displayName}}]</a> </li> <li *ngIf="isLoggedIn" class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/protectedPage']">Protected Page</a> </li> </ul> </div> </nav>
تکمیل کامپوننت صفحهی محافظت شده
در تصویر قبل، نمایش توکن decode شده را نیز مشاهده کردید. این نمایش توسط کامپوننت صفحهی محافظت شده، مدیریت میشود:
import { Component, OnInit } from "@angular/core"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-protected-page", templateUrl: "./protected-page.component.html", styleUrls: ["./protected-page.component.css"] }) export class ProtectedPageComponent implements OnInit { decodedAccessToken: any = {}; accessTokenExpirationDate: Date = null; constructor(private authService: AuthService) { } ngOnInit() { this.decodedAccessToken = this.authService.getDecodedAccessToken(); this.accessTokenExpirationDate = this.authService.getAccessTokenExpirationDate(); } }
<h1> Decoded Access Token </h1> <div class="alert alert-info"> <label> Access Token Expiration Date:</label> {{accessTokenExpirationDate}} </div> <div> <pre>{{decodedAccessToken | json}}</pre> </div>
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
با گسترش استفاده از کامپیوتر در بسیاری از امور روزمره انسانها سازگار بودن برنامهها با سلیقه کاربران به یکی از نیازهای اصلی برنامههای کامپیوتری تبدیل شده است. بدون شک زبان و فرهنگ یکی از مهمترین عوامل در ایجاد ارتباط نزدیک بین برنامه و کاربر به شمار میرود و نقشی غیر قابل انکار در میزان موفقیت یک برنامه به عهده دارد. از این رو در این نوشته تلاش بر آن است تا یکی از سادهترین و در عین حال کاراترین راههای ممکن برای ایجاد برنامههای چند زبانه با استفاده از تکنولوژی WPF آموزش داده شود.
مروری بر روشهای موجود
همواره روشهای مختلفی برای پیاده سازی یک ایده در دنیای نرم افزار وجود دارد که هر روش را میتوان بر حسب نیاز مورد استفاده قرار داد. در برنامههای مبتنی بر WPF معمولا از دو روش عمده برای این منظور استفاده میشود:
1-استفاده از فایلهای resx
در این روش که برای Win App نیز استفاده میشود، اطلاعات مورد نیاز برای هر زبان به شکل جدول هایی دارای کلید و مقدار در داخل یک فایل .resx نگهداری میشود و در زمان اجرای برنامه بر اساس انتخاب کاربر اطلاعات زبان مورد نظر از داخل فایل resx خوانده شده و نمایش داده میشود. یکی از ضعف هایی که این روش در عین ساده بودن دارد این است که همه اطلاعات مورد نیاز داخل assembly اصلی برنامه قرار میگیرد و امکان افزودن زبانهای جدید بدون تغییر دادن برنامه اصلی ممکن نخواهد بود.
2-استفاده از فایلهای csv که به فایلهای dll تبدیل میشوند
در این روش با استفاده از ابزارهای موجود در کامپایلر WPF برای هر کنترل یک property به نام Uid ایجاد شده و مقدار دهی میشود. سپس با ابزار دیگری ( که جزو ابزارهای کامپایلر محسوب نمیشود ) از فایل csproj پروژه یک خروجی اکسل با فرمت csv ایجاد میشود که شامل Uidهای کنترلها و مقادیر آنها است. پس از ترجمه متون مورد نظر به زبان مقصد با کمک ابزار دیگری فایل اکسل مورد نظر به یک net assembly تبدیل میشود و داخل پوشه ای با نام culture استاندارد ذخیره میشود. ( مثلا برای زبان فارسی نام پوشه fa-IR خواهد بود ). زمانی که برنامه اجرا میشود بر اساس culture ای که در سیستم عامل انتخاب شده است و در صورتی که برای آن culture فایل dll ای موجود باشد، زبان مربوط به آن culture را load خواهد کرد. با وجود این که این روش مشکل روش قبلی را ندارد و بیشتر با ویژگیهای WPF سازگار است اما پروسه ای طولانی برای انجام کارها دارد و به ازای هر تغییری باید کل مراحل هر بار تکرار شوند. همچنین مشکلاتی در نمایش برخی زبانها ( از جمله فارسی ) در این روش مشاهده شده است.
روش سوم!
روش سوم اما کاملا بر پایه WPF و در اصطلاح WPF-Native میباشد. ایده از آنجا ناشی شده است که برای ایجاد skin در برنامههای WPF استفاده میشود. در ایجاد برنامههای Skin-Based به این شیوه عمل میشود که skinهای مورد نظر به صورت style هایی در داخل ResourceDictionary ها قرار میگیرند. سپس آن ResourceDictionary به شکل dll کامپایل میشود. در برنامه اصلی نیز همه کنترلها style هایشان را به شکل dynamic resource از داخل یک ResourceDictionary مشخص شده load میکنند. حال کافی است برای تغییر skin فعلی، ResourceDictionary مورد نظر از dll مشخص load شود و ResourceDictionary ای که در حال حاضر در برنامه از آن استفاده میشود با ResourceDictionary ای که load شده جایگزین شود. کنترلها مقادیر جدید را از ResourceDictionary جدید به شکل کاملا خودکار دریافت خواهند کرد.
به سادگی میتوان از این روش برای تغییر زبان برنامه نیز استفاده کرد با این تفاوت که این بار، به جای Style ها، Stringهای زبانهای مختلف را درون resourceها نگهداری خواهیم کرد.
یک مثال ساده
در این قسمت نحوه پیاده سازی این روش با ایجاد یک نمونه برنامه ساده که دارای دو زبان انگلیسی و فارسی خواهد بود آموزش داده میشود.
ابتدا یک پروژه WPF Application در Visual Studio 2010 ایجاد کنید. در MainWindow سه کنترل Button قرار دهید و یک ComboBox که قرار است زبانهای موجود را نمایش دهد و با انتخاب یک زبان، نوشتههای درون Buttonها متناسب با آن تغییر خواهند کرد.
توجه داشته باشید که برای Buttonها نباید به صورت مستقیم مقداری به Content شان داده شود. زیرا مقدار مورد نظر از داخل ResourceDictionary که خواهیم ساخت به شکل dynamic گرفته خواهد شد. پس در این مرحله یک ResourceDictionary به پروژه اضافه کرده و در آن resource هایی به شکل string ایجاد میکنیم. هر resource دارای یک Key میباشد که بر اساس آن، Button مورد نظر، مقدار آن Resource را load خواهد کرد. فایل ResourceDictionary را
Culture_en-US.xaml نامگذاری کنید و مقادیر مورد نظر را به آن اضافه نمایید.
دقت کنید که namespace ای که کلاس string در آن قرار دارد به فایل xaml اضافه شده است و پیشوند system به آن نسبت داده شده است.
با افزودن یک ResourceDictionary به پروژه، آن ResourceDictionary به MergedDictionary کلاس App اضافه میشود. بنابراین فایل App.xaml به شکل زیر خواهد بود:
برای اینکه بتوانیم محتوای Buttonهای موجود را به صورت داینامیک و در زمان اجرای برنامه، از داخل Resourceها بگیریم، از DynamicResource استفاده میکنیم.
بسیار خوب! اکنون باید شروع به ایجاد یک ResourceDictionary برای زبان فارسی کنیم و آن را به صورت یک فایل dll کامپایل نماییم.
برای این کار یک پروژه جدید در قسمت WPF از نوع User control ایجاد میکنیم و نام آن را Culture_fa-IR_Farsi قرار میدهیم. لطفا شیوه نامگذاری را رعایت کنید چرا که در ادامه به آن نیاز خواهیم داشت.
پس از ایجاد پروژه فایل UserControl1.xaml را از پروژه حذف کنید و یک ResourceDictionary با نام Culture_fa-IR.xaml اضافه کنید. محتوای آن را پاک کنید و محتوای فایل Culture_en-US.xaml را از پروژه قبلی به صورت کامل در فایل جدید کپی کنید. دو فایل باید ساختار کاملا یکسانی از نظر key برای Resourceهای موجود داشته باشند. حالا زمان ترجمه فرا رسیده است! رشتههای دلخواه را ترجمه کنید و پروژه را build نمایید.
پس از ترجمه فایل Culture_fa-IR.xaml به شکل زیر خواهد بود:
خروجی این پروژه یک فایل با نام Culture_fa-IR_Farsi.dll خواهد بود که حاوی یک ResourceDictionary برای زبان فارسی میباشد.
در ادامه میخواهیم راهکاری ارئه دهیم تا بتوان فایلهای dll مربوط به زبانها را در زمان اجرای برنامه اصلی، load کرده و نام زبانها را در داخل ComboBox ای که داریم نشان دهیم. سپس با انتخاب هر زبان در ComboBox، محتوای Buttonها بر اساس زبان انتخاب شده تغییر کند.
برای سهولت کار، نام فایلها را به گونه ای انتخاب کردیم که بتوانیم سادهتر به این هدف برسیم. نام هر فایل از سه بخش تشکیل شده است:
پوشه ای با نام Languages در کنار فایل اجرایی برنامه اصلی ایجاد کنید و فایل Culture_fa-IR_Farsi.dll را درون آن کپی کنید. تصمیم داریم همه dllهای مربوط به زبانها را داخل این پوشه قرار دهیم تا مدیریت آنها سادهتر شود.
برای مدیریت بهتر فایلهای مربوط به زبانها یک کلاس با نام CultureAssemblyModel خواهیم ساخت که هر instance از آن نشانگر یک فایل زبان خواهد بود. یک کلاس با این نام به پروژه اضافه کنید و propertyهای زیر را در آن تعریف نمایید:
اکنون باید لیست cultureهای موجود را از داخل پوشه languages خوانده و نام آنها را در ComboBox نمایش دهیم.
برای خواندن لیست cultureهای موجود، لیستی از CultureAssmeblyModelها ایجاد کرده و با استفاده از متد LoadCultureAssmeblies، آن را پر میکنیم.
پس از دریافت اطلاعات cultureهای موجود، زمان نمایش آنها در ComboBox است. این کار بسیار ساده است، تنها کافی است ItemsSource آن را با لیستی از CultureAssmeblyModelها که ساختیم، مقدار دهی کنیم.
البته لازم به ذکر است که برای نمایش فقط نام هر CultureAssemblyModel در ComboBox، باید ItemTemplate مناسبی برای ComboBox ایجاد کنیم. در مثال ما ItemTemplate به شکل زیر خواهد بود:
توجه داشته باشید که با وجود اینکه فقط نام را در ComboBox نشان میدهیم، اما باز هم هر آیتم از ComboBox یک instance از نوع CultureAssemblyModel میباشد.
در مرحله بعد، قرار است متدی بنویسیم که اطلاعات زبان انتخاب شده را گرفته و با جابجایی ResourceDictionary ها، زبان برنامه را تغییر دهیم.
متدی با نام LoadCulture در کلاس App ایجاد میکنیم که یک CultureAssemblyModel به عنوان ورودی دریافت کرده و ResourceDictionary داخل آن را load میکند و آن را با ResourceDictionary فعلی موجود در App.xaml جابجا مینماید.
با این کار، Button هایی که قبلا مقدار Content خود را از Resourceهای موجود دریافت میکردند، اکنون از Resourceهای جابجا شده خواهند گرفت و به این ترتیب زبان انتخاب شده بر روی برنامه اعمال میشود.
برای ارسال زبان انتخاب شده به این متد، باید رویداد SelectionChanged را برای ComboBox مدیریت کنیم:
کار انجام شد!
از مزیتهای این روش میتوان به WPF-Native بودن، سادگی در پیاده سازی، قابلیت load کردن هر زبان جدیدی در زمان اجرا بدون نیاز به کوچکترین تغییر در برنامه اصلی و همچنین پشتیبانی کامل از نمایش زبانهای مختلف از جمله فارسی اشاره کرد.
مروری بر روشهای موجود
همواره روشهای مختلفی برای پیاده سازی یک ایده در دنیای نرم افزار وجود دارد که هر روش را میتوان بر حسب نیاز مورد استفاده قرار داد. در برنامههای مبتنی بر WPF معمولا از دو روش عمده برای این منظور استفاده میشود:
1-استفاده از فایلهای resx
در این روش که برای Win App نیز استفاده میشود، اطلاعات مورد نیاز برای هر زبان به شکل جدول هایی دارای کلید و مقدار در داخل یک فایل .resx نگهداری میشود و در زمان اجرای برنامه بر اساس انتخاب کاربر اطلاعات زبان مورد نظر از داخل فایل resx خوانده شده و نمایش داده میشود. یکی از ضعف هایی که این روش در عین ساده بودن دارد این است که همه اطلاعات مورد نیاز داخل assembly اصلی برنامه قرار میگیرد و امکان افزودن زبانهای جدید بدون تغییر دادن برنامه اصلی ممکن نخواهد بود.
2-استفاده از فایلهای csv که به فایلهای dll تبدیل میشوند
در این روش با استفاده از ابزارهای موجود در کامپایلر WPF برای هر کنترل یک property به نام Uid ایجاد شده و مقدار دهی میشود. سپس با ابزار دیگری ( که جزو ابزارهای کامپایلر محسوب نمیشود ) از فایل csproj پروژه یک خروجی اکسل با فرمت csv ایجاد میشود که شامل Uidهای کنترلها و مقادیر آنها است. پس از ترجمه متون مورد نظر به زبان مقصد با کمک ابزار دیگری فایل اکسل مورد نظر به یک net assembly تبدیل میشود و داخل پوشه ای با نام culture استاندارد ذخیره میشود. ( مثلا برای زبان فارسی نام پوشه fa-IR خواهد بود ). زمانی که برنامه اجرا میشود بر اساس culture ای که در سیستم عامل انتخاب شده است و در صورتی که برای آن culture فایل dll ای موجود باشد، زبان مربوط به آن culture را load خواهد کرد. با وجود این که این روش مشکل روش قبلی را ندارد و بیشتر با ویژگیهای WPF سازگار است اما پروسه ای طولانی برای انجام کارها دارد و به ازای هر تغییری باید کل مراحل هر بار تکرار شوند. همچنین مشکلاتی در نمایش برخی زبانها ( از جمله فارسی ) در این روش مشاهده شده است.
روش سوم!
روش سوم اما کاملا بر پایه WPF و در اصطلاح WPF-Native میباشد. ایده از آنجا ناشی شده است که برای ایجاد skin در برنامههای WPF استفاده میشود. در ایجاد برنامههای Skin-Based به این شیوه عمل میشود که skinهای مورد نظر به صورت style هایی در داخل ResourceDictionary ها قرار میگیرند. سپس آن ResourceDictionary به شکل dll کامپایل میشود. در برنامه اصلی نیز همه کنترلها style هایشان را به شکل dynamic resource از داخل یک ResourceDictionary مشخص شده load میکنند. حال کافی است برای تغییر skin فعلی، ResourceDictionary مورد نظر از dll مشخص load شود و ResourceDictionary ای که در حال حاضر در برنامه از آن استفاده میشود با ResourceDictionary ای که load شده جایگزین شود. کنترلها مقادیر جدید را از ResourceDictionary جدید به شکل کاملا خودکار دریافت خواهند کرد.
به سادگی میتوان از این روش برای تغییر زبان برنامه نیز استفاده کرد با این تفاوت که این بار، به جای Style ها، Stringهای زبانهای مختلف را درون resourceها نگهداری خواهیم کرد.
یک مثال ساده
در این قسمت نحوه پیاده سازی این روش با ایجاد یک نمونه برنامه ساده که دارای دو زبان انگلیسی و فارسی خواهد بود آموزش داده میشود.
ابتدا یک پروژه WPF Application در Visual Studio 2010 ایجاد کنید. در MainWindow سه کنترل Button قرار دهید و یک ComboBox که قرار است زبانهای موجود را نمایش دهد و با انتخاب یک زبان، نوشتههای درون Buttonها متناسب با آن تغییر خواهند کرد.
توجه داشته باشید که برای Buttonها نباید به صورت مستقیم مقداری به Content شان داده شود. زیرا مقدار مورد نظر از داخل ResourceDictionary که خواهیم ساخت به شکل dynamic گرفته خواهد شد. پس در این مرحله یک ResourceDictionary به پروژه اضافه کرده و در آن resource هایی به شکل string ایجاد میکنیم. هر resource دارای یک Key میباشد که بر اساس آن، Button مورد نظر، مقدار آن Resource را load خواهد کرد. فایل ResourceDictionary را
Culture_en-US.xaml نامگذاری کنید و مقادیر مورد نظر را به آن اضافه نمایید.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> <system:String x:Key="button1">Hello!</system:String> <system:String x:Key="button2">How Are You?</system:String> <system:String x:Key="button3">Are You OK?</system:String> </ResourceDictionary>
دقت کنید که namespace ای که کلاس string در آن قرار دارد به فایل xaml اضافه شده است و پیشوند system به آن نسبت داده شده است.
با افزودن یک ResourceDictionary به پروژه، آن ResourceDictionary به MergedDictionary کلاس App اضافه میشود. بنابراین فایل App.xaml به شکل زیر خواهد بود:
<Application x:Class="BeRMOoDA.WPF.LocalizationSample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"> <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Culture_en-US.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application>
برای اینکه بتوانیم محتوای Buttonهای موجود را به صورت داینامیک و در زمان اجرای برنامه، از داخل Resourceها بگیریم، از DynamicResource استفاده میکنیم.
<Button Content="{DynamicResource ResourceKey=button1}" /> <Button Content="{DynamicResource ResourceKey=button2}" /> <Button Content="{DynamicResource ResourceKey=button3}" />
بسیار خوب! اکنون باید شروع به ایجاد یک ResourceDictionary برای زبان فارسی کنیم و آن را به صورت یک فایل dll کامپایل نماییم.
برای این کار یک پروژه جدید در قسمت WPF از نوع User control ایجاد میکنیم و نام آن را Culture_fa-IR_Farsi قرار میدهیم. لطفا شیوه نامگذاری را رعایت کنید چرا که در ادامه به آن نیاز خواهیم داشت.
پس از ایجاد پروژه فایل UserControl1.xaml را از پروژه حذف کنید و یک ResourceDictionary با نام Culture_fa-IR.xaml اضافه کنید. محتوای آن را پاک کنید و محتوای فایل Culture_en-US.xaml را از پروژه قبلی به صورت کامل در فایل جدید کپی کنید. دو فایل باید ساختار کاملا یکسانی از نظر key برای Resourceهای موجود داشته باشند. حالا زمان ترجمه فرا رسیده است! رشتههای دلخواه را ترجمه کنید و پروژه را build نمایید.
پس از ترجمه فایل Culture_fa-IR.xaml به شکل زیر خواهد بود:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:system="clr-namespace:System;assembly=mscorlib"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Culture_fa-IR_Farsi.xaml"/> </ResourceDictionary.MergedDictionaries> <system:String x:Key="button1">سلام!</system:String> <system:String x:Key="button2">حالت چطوره؟</system:String> <system:String x:Key="button3">خوبی؟</system:String> </ResourceDictionary>
در ادامه میخواهیم راهکاری ارئه دهیم تا بتوان فایلهای dll مربوط به زبانها را در زمان اجرای برنامه اصلی، load کرده و نام زبانها را در داخل ComboBox ای که داریم نشان دهیم. سپس با انتخاب هر زبان در ComboBox، محتوای Buttonها بر اساس زبان انتخاب شده تغییر کند.
برای سهولت کار، نام فایلها را به گونه ای انتخاب کردیم که بتوانیم سادهتر به این هدف برسیم. نام هر فایل از سه بخش تشکیل شده است:
Culture_[standard culture notation]_[display name for this culture].dll
یعنی
اگر فایل Culture_fa-IR_Farsi.dll را در نظر بگیریم، Culture نشان دهنده
این است که این فایل مربوط به یک culture میباشد. fa-IR نمایش استاندارد
culture برای کشور ایران و زبان فارسی است و Farsi هم مقداری است که میخواهیم در ComboBox برای این زبان نمایش داده شود.پوشه ای با نام Languages در کنار فایل اجرایی برنامه اصلی ایجاد کنید و فایل Culture_fa-IR_Farsi.dll را درون آن کپی کنید. تصمیم داریم همه dllهای مربوط به زبانها را داخل این پوشه قرار دهیم تا مدیریت آنها سادهتر شود.
برای مدیریت بهتر فایلهای مربوط به زبانها یک کلاس با نام CultureAssemblyModel خواهیم ساخت که هر instance از آن نشانگر یک فایل زبان خواهد بود. یک کلاس با این نام به پروژه اضافه کنید و propertyهای زیر را در آن تعریف نمایید:
public class CultureAssemblyModel { //the text will be displayed to user as language name (like Farsi) public string DisplayText { get; set; } //name of .dll file (like Culture_fa-IR_Farsi.dll) public string Name { get; set; } //standar notation of this culture (like fa-IR) public string Culture { get; set; } //name of resource dictionary file inside the loaded .dll (like Culture_fa-IR.xaml) public string XamlFileName { get; set; } }
برای خواندن لیست cultureهای موجود، لیستی از CultureAssmeblyModelها ایجاد کرده و با استفاده از متد LoadCultureAssmeblies، آن را پر میکنیم.
//will keep information about loaded assemblies public List<CultureAssemblyModel> CultureAssemblies { get; set; } //loads assmeblies in languages folder and adds their info to list void LoadCultureAssemblies() { //we should be sure that list is empty before adding info (do u want to add some cultures more than one? of course u dont!) CultureAssemblies.Clear(); //creating a directory represents applications directory\languages DirectoryInfo dir = new DirectoryInfo(System.IO.Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) + "\\languages"); //getting all .dll files in the language folder and its sub dirs. (who knows? maybe someone keeps each culture file in a seperate folder!) var assemblies = dir.GetFiles("*.dll", SearchOption.AllDirectories); //for each found .dll we will create a model and set its properties and then add to list for (int i = 0; i < assemblies.Count(); i++) {
string name = assemblies[i].Name;
CultureAssemblyModel model = new CultureAssemblyModel() { DisplayText = name.Split('.', '_')[2], Culture = name.Split('.', '_')[1], Name = name , XamlFileName =name.Substring(0, name.LastIndexOf(".")) + ".xaml" }; CultureAssemblies.Add(model); } }
comboboxLanguages.ItemsSource = CultureAssemblies;
<ComboBox HorizontalAlignment="Left" Margin="10" VerticalAlignment="Top" MinWidth="100" Name="comboboxLanguages"> <ComboBox.ItemTemplate> <DataTemplate> <Label Content="{Binding DisplayText}"/> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>
در مرحله بعد، قرار است متدی بنویسیم که اطلاعات زبان انتخاب شده را گرفته و با جابجایی ResourceDictionary ها، زبان برنامه را تغییر دهیم.
متدی با نام LoadCulture در کلاس App ایجاد میکنیم که یک CultureAssemblyModel به عنوان ورودی دریافت کرده و ResourceDictionary داخل آن را load میکند و آن را با ResourceDictionary فعلی موجود در App.xaml جابجا مینماید.
با این کار، Button هایی که قبلا مقدار Content خود را از Resourceهای موجود دریافت میکردند، اکنون از Resourceهای جابجا شده خواهند گرفت و به این ترتیب زبان انتخاب شده بر روی برنامه اعمال میشود.
//loads selected culture public void LoadCulture(CultureAssemblyModel culture) { //creating a FileInfo object represents .dll file of selected cultur FileInfo assemblyFile = new FileInfo("languages\\" + culture.Name); //loading .dll into memory as a .net assembly var assembly = Assembly.LoadFile(assemblyFile.FullName); //getting .dll file name var assemblyName = assemblyFile.Name.Substring(0, assemblyFile.Name.LastIndexOf(".")); //creating string represents structure of a pack uri (something like this: /{myassemblyname;component/myresourcefile.xaml} string packUri = string.Format(@"/{0};component/{1}", assemblyName, culture.XamlFileName); //creating a pack uri Uri uri = new Uri(packUri, UriKind.Relative); //now we have created a pack uri that represents a resource object in loaded assembly //and its time to load that as a resource dictionary (do u remember that we had resource dictionary in culture assemblies? don't u?) var dic = Application.LoadComponent(uri) as ResourceDictionary; dic.Source = uri; //here we will remove current merged dictionaries in our resource dictionary and add recently-loaded resource dictionary as e merged dictionary var mergedDics = this.Resources.MergedDictionaries; if (mergedDics.Count > 0) mergedDics.Clear(); mergedDics.Add(dic); }
void comboboxLanguages_SelectionChanged(object sender, SelectionChangedEventArgs e) { var selectedCulture = (CultureAssemblyModel)comboboxLanguages.SelectedItem; App app = Application.Current as App; app.LoadCulture(selectedCulture); }
کار انجام شد!
از مزیتهای این روش میتوان به WPF-Native بودن، سادگی در پیاده سازی، قابلیت load کردن هر زبان جدیدی در زمان اجرا بدون نیاز به کوچکترین تغییر در برنامه اصلی و همچنین پشتیبانی کامل از نمایش زبانهای مختلف از جمله فارسی اشاره کرد.
انتشار نوعها (Types) به یک ماژول
در این قسمت به نحوهی تبدیل سورس به یک فایل قابل انتشار میپردازیم. کد زیر را به عنوان مثال در نظر بگیرید:
در این قسمت به نحوهی تبدیل سورس به یک فایل قابل انتشار میپردازیم. کد زیر را به عنوان مثال در نظر بگیرید:
public sealed class Program { public static void Main() { System.Console.WriteLine("Hi"); } }
این کد یک ارجاع به نام کنسول دارد که این ارجاع، داخل فایلی به نام mscorlib.dll قرار دارد. پس برنامهی ما نوعی را دارد، که آن نوع توسط شرکت دیگری پیاده سازی شده است. برای ساخت برنامهی کد بالا، کدها را داخل فایلی با نام program.cs قرار داده و با دستور زیر در خط فرمان آن را کامپایل میکنیم:
csc.exe /out:Program.exe /t:exe /r:MSCorLib.dll Program.cs
موقعیکه کامپایلر فایل سورس را مورد بررسی قرار میدهد، متوجه متد writerline میگردد؛ ولی از آنجاکه این نوع توسط شما ایجاد نشده است و یک نوع خارجی است، شما باید یک مجموعه از ارجاعات را به کمپایلر داده تا آن نوع را در آنها بیابد. ارائه این ارجاعات به کامپایلر توسط سوئیچ r/ که در خط بالا استفاده شده است، صورت میگیرد.
mscorlib یک فایل سورس است که شامل همهی نوعها، از قبیل int,string,byte و خیلی از نوعهای دیگر میشود. از آنجائیکه استفادهی از این نوعها به طور مکرر توسط برنامه نویسها صورت میگیرد، کمپایلر به طور خودکار این کتابخانه را به لیست ارجاعات اضافه میکند. به بیان دیگر خط بالا به شکل زیر هم قابل اجراست:
mscorlib یک فایل سورس است که شامل همهی نوعها، از قبیل int,string,byte و خیلی از نوعهای دیگر میشود. از آنجائیکه استفادهی از این نوعها به طور مکرر توسط برنامه نویسها صورت میگیرد، کمپایلر به طور خودکار این کتابخانه را به لیست ارجاعات اضافه میکند. به بیان دیگر خط بالا به شکل زیر هم قابل اجراست:
csc.exe /out:Program.exe /t:exe Program.cs
به علاوه از آنجایی که بقیهی سوئیچها هم مقدار پیش فرضی را دارند، خط زیر هم معادل خطوط بالاست:
csc.exe Program.cs
اگر به هر دلیلی دوست ندارید که سمت mscorlib ارجاعی صورت بگیرید، میتوانید از دستور زیر استفاده کنید:
/nostdlib
مایکروسافت موقعی از این سوئیچ بالا استفاده کرده است که خواسته است خود mscorlib را بسازد. با اضافه کردن این سوئیچ، کد مثال که حاوی شیء یا نوع کنسول است به خطا برخواهد خورد چون تعریف آن در mscorlib صورت گرفته و شما با سوئیچ بالا دسترسی به آن را ممنوع اعلام کردهاید و از آنجاکه این نوع تعریف نشده، برنامه ازکامپایل بازخواهد ماند.
csc.exe /out:Program.exe /t:exe /nostdlib Program.cs
بیایید نگاه دقیقتری به فایل program.exe ساخته شده بیندازیم؛ دقیقا این فایل چه نوع فایلی است؟ برای بسیاری از مبتدیان، این یک فایل اجرایی است که در هر دو ماشین 32 و 64 بیتی قابل اجراست. ویندوز از سه نوع برنامه پشتیبانی میکند: CUI یا برنامههای تحت کنسول، برنامههایی با رابط گرافیکی GUI و برنامههای مخصوص windows store که سوئیچهای آن به شرح زیر است:
//CUI /t:exe //GUI /t:winexe //Winsows store App /t:appcontainerexe
قبل از اینکه بحث را در مورد سوئیچها به پایان برسانیم، اجازه دهید در مورد فایلهای پاسخگو یا response file صحبت کنیم. یک فایل پاسخگو، فایلی است که شامل مجموعهای از سوئیچهای خط فرمان میشود. موقعیکه csc.exe اجرا میشود، به فایل پاسخگویی که شما به آن معرفی کردهاید مراجعه کرده و فرمان را با سوئیچهای داخل آن اجرا میکند. معرفی یک فایل پاسخگو به کامپایلر توسط علامت @ و سپس نام فایل صورت میگیرد و در این فایل هر خط، شامل یک سوئیچ است. مثلا فایل پاسخگوی response.rsp شامل سوئیچهای زیر است:
/out:MyProject.exe /target:winexe
و برای در نظر گرفتن این سوئیچها فایل پاسخگو را به کامپایلر معرفی میکنیم:
csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs
این فایل خیلی کار شما را راحت میکند و نمیگذارد در هر بار کامپایل، مرتب سوئیچهای آن را وارد کنید و کیفیت کار را بالا میبرد. همچنین میتوانید چندین فایل پاسخگو داشته باشید و هر کدام شامل سوئیچهای مختلفی تا اگر خواستید تنظیمات کامپایل را تغییر دهید، به راحتی تنها نام فایل پاسخگو را تغییر دهید. همچنین کامپایلر سی شارپ از چندین فایل پاسخگو هم پشتیبانی میکند و میتوانید هر تعداد فایل پاسخگویی را به آن معرفی کنید. در صورتیکه فایل را با نام csc.rsp نامگذاری کرده باشید، نیازی به معرفی آن
نیست چرا که کامپایلر در صورت وجود، آن را به طور خودکار خواهد خواند و به
این فایل global response file یا فایل پاسخگوی عمومی گویند.
در صورتیکه چندین فایل پاسخگو که به آن فایلهای محلی local میگویند، معرفی کنید که دستورات آنها(سوئیچ) با دستورات داخل csc.rsp مقدار متفاوتی داشته باشند، فایلهای محلی الویت بالاتری نسبت به فایل global داشته و تنظمیات آنها روی فایل global رونوشت میگردند.
موقعیکه شما دات نت فریمورک را نصب میکنید، فایل csc.rsp را با تنظیمات پیش فرض در مسیر زیر نصب میکند:
در صورتیکه چندین فایل پاسخگو که به آن فایلهای محلی local میگویند، معرفی کنید که دستورات آنها(سوئیچ) با دستورات داخل csc.rsp مقدار متفاوتی داشته باشند، فایلهای محلی الویت بالاتری نسبت به فایل global داشته و تنظمیات آنها روی فایل global رونوشت میگردند.
موقعیکه شما دات نت فریمورک را نصب میکنید، فایل csc.rsp را با تنظیمات پیش فرض در مسیر زیر نصب میکند:
%SystemRoot%\ Microsoft.NET\Framework(64)\vX.X.X
حروف x نمایانگر نسخهی دات نت فریمورکی هست که شما نصب کردهاید. آخرین ورژن از این فایل در زمان نگارش کتاب، شامل سوئیچهای زیر بوده است.
# This file contains commandline options that the C# # command line compiler (CSC) will process as part # of every compilation, unless the "/noconfig" option # is specified. # Reference the common Framework libraries /r:Accessibility.dll /r:Microsoft.CSharp.dll /r:System.Configuration.dll /r:System.Configuration.Install.dll /r:System.Core.dll /r:System.Data.dll /r:System.Data.DataSetExtensions.dll /r:System.Data.Linq.dll /r:System.Data.OracleClient.dll /r:System.Deployment.dll /r:System.Design.dll /r:System.DirectoryServices.dll /r:System.dll /r:System.Drawing.Design.dll /r:System.Drawing.dll /r:System.EnterpriseServices.dll /r:System.Management.dll /r:System.Messaging.dll /r:System.Runtime.Remoting.dll /r:System.Runtime.Serialization.dll /r:System.Runtime.Serialization.Formatters.Soap.dll /r:System.Security.dll /r:System.ServiceModel.dll /r:System.ServiceModel.Web.dll /r:System.ServiceProcess.dll /r:System.Transactions.dll /r:System.Web.dll /r:System.Web.Extensions.Design.dll /r:System.Web.Extensions.dll /r:System.Web.Mobile.dll /r:System.Web.RegularExpressions.dll /r:System.Web.Services.dll /r:System.Windows.Forms.Dll /r:System.Workflow.Activities.dll /r:System.Workflow.ComponentModel.dll /r:System.Workflow.Runtime.dll /r:System.Xml.dll /r:System.Xml.Linq.dll
این فایل حاوی بسیاری از ارجاعات اسمبلیهایی است که بیشتر توسط توسعه دهندگان مورد استفاده قرار میگیرد و در صورتیکه برنامهی شما به این اسمبلیها محدود میگردد، لازم نیست که این اسمبلیها را به کامپایلر معرفی کنید.
البته ارجاع کردن به این اسمبلیها تا حد کمی باعث کند شدن صورت کامپایل میشوند؛ ولی تاثیری بر فایل نهایی و نحوهی اجرای آن نمیگذارند.
البته ارجاع کردن به این اسمبلیها تا حد کمی باعث کند شدن صورت کامپایل میشوند؛ ولی تاثیری بر فایل نهایی و نحوهی اجرای آن نمیگذارند.
نکته: در صورتی که قصد ارجاعی را دارید، میتوانید آدرس مستقیم اسمبلی را هم ذکر کنید. ولی اگر تنها به نام اسمبلی اکتفا کنید، مسیرهای زیر جهت یافتن اسمبلی بررسی خواهند شد:
- دایرکتوری برنامه
- دایرکتوری که شامل فایل csc.exe میشود. که خود فایل mscorlib از همانجا خوانده میشود و مسیر آن شبیه مسیر زیر است:
%SystemRoot%\Microsoft.NET\Framework\v4.0.#####
- هر دایرکتوری که توسط سوئیچ lib/ مشخص شده باشد.
- هر دایرکتوری که توسط متغیر محلی lib مشخص شده باشد.
استفاده از سوئیچ noconfig/ هم باعث میشود که فایلهای پاسخگوی از هر نوعی، چه عمومی و چه محلی، مورد استفاده قرار نگیرند. همچنین شما مجاز هستید که فایل csc.rsp را هم تغییر دهید؛ ولی این نکته را فراموش نکنید، در صورتی که برنامهی شما به سیستمی دیگر منتقل شود، تنظیمات این فایل در آنجا متفاوت خواهد بود و بهتر هست یک فایل محلی را که همراه خودش هست استفاده کنید.
در قسمت بعدی نگاه دیگری بر متادیتا خواهیم داشت.دلایل شانه خالی کردن از آزمایش واحد!
1- نوشتن آزمایشات زمان زیادی را به خود اختصاص خواهند داد.
مهمترین دلیلی که برنامهنویسها به سبب آن از نوشتن آزمایشات واحد امتناع میکنند، همین موضوع است. اکثر افراد به آزمایش بهعنوان مرحله آخر توسعه فکر میکنند. اگر این چنین است، بله! نوشتن آزمایشهای واحد واقعا سخت و زمانگیر خواهند بود. به همین جهت برای جلوگیری از این مساله روش pay-as-you-go مطرح شده است (ماخذ: کتاب Pragmatic Unit Testing در سی شارپ). یعنی با اضافه شدن هر واحد کوچکی به سیستم، آزمایش واحد آنرا نیز تهیه کنید. به این صورت در طول توسعه سیستم با باگهای کمتری نیز برخورد خواهید داشت چون اجزای آنرا در این حین به تفصیل مورد بررسی قرار دادهاید. اثر این روش را در شکل زیر میتوانید ملاحظه نمائید (تصویری از همان کتاب ذکر شده)
نوشتن آزمایشات واحد زمانبر هستند اما توسعه پیوسته آنها با به تاخیر انداختن آزمایشات به انتهای پروژه، همانند تصویر فوق تاثیر بسیار قابل توجهی در بهره وری شما خواهند داشت.
بنابراین اگر عنوان میکنید که وقت ندارید آزمایش واحد بنویسید، به چند سؤال زیر پاسخ دهید:
الف) چه مقدار زمان را صرف دیباگ کردن کدهای خود یا دیگران میکنید؟
ب) چه میزان زمان را صرف بازنویسی کدی کردهاید که تصور میرفت درست کار میکند اما اکنون بسیار مشکل زا ظاهر شده است؟
ج) چه مقدار زمان را صرف این کردهاید که منشاء باگ گزارش شده در برنامه را کشف کنید؟
برای افرادی که آزمایشات واحد را در حین پروسه توسعه در نظر نمیگیرند، این مقادیر بالا است و با ازدیاد تعداد خطوط سورس کدها، این ارقام سیر صعودی خواهند داشت.
تصویری از کتاب xUnit Test Patterns ، که بیانگر کاهش زمان و هزینه کد نویسی در طول زمان با رعایت اصول آزمایشات واحد است
2- اجرای آزمایشات واحد زمان زیادی را تلف میکند.
نباید اینطور باشد. عموما اجرای هزاران آزمایش واحد، باید در کسری از ثانیه صورت گیرد. (برای اطلاعات بیشتر به قسمت حد و مرز یک آزمایش واحد در قسمت قبل مراجعه نمائید)
3- امکان تهیه آزمایشات واحد برای کدهای قدیمی ( legacy code ) من وجود ندارد
برای بسیاری از برنامه نویسها، تهیه آزمایش واحد برای کدهای قدیمی بسیار مشکل است زیرا شکستن آنها به واحدهای کوچکتر قابل آزمایش بسیار خطرناک و پرهزینه است و ممکن است سبب از کار افتادن سیستم آنها گردد. اینجا مشکل از آزمایش واحد نیست. مشکل از ضعف برنامه نویسی آن سیستم است. روش refactoring ، طراحی مجدد و نوشتن آزمایشات واحد، به تدریج سبب طراحی بهتر برنامه از دیدگاههای شیءگرایی شده و نگهداری سیستم را در طولانی مدت سادهتر میسازد. آزمایشات واحد این نوع سیستمها را از حالت فلج بودن خارج میسازد.
4- کار من نیست که کدهای نوشته شده را آزمایش کنم!
باید درنظر داشته باشید که این هم کار شما نیست که انبوهی از کدهای مشکل دار را به واحد بررسی کننده آن تحویل دهید! همچنین اگر تیم آزمایشات و کنترل کیفیت به این نتیجه برسد که عموما از کدهای شما کمتر میتوان باگ گرفت، این امر سبب معروفیت و تضمین شغلی شما خواهد شد.
همچنین این کار شما است که تضمین کنید واحد تهیه شده مقصود مورد نظر را ارائه میدهد و اینکار را با ارائه یک یا چندین آزمایش واحد میتوان اثبات کرد.
5- تنها قسمتی از سیستم به من واگذار شده است و من دقیقا نمیدانم که رفتار کلی آن چیست. بنابراین آن را نمیتوانم آزمایش کنم!
اگر واقعا نمیدانید که این کد قرار است چه کاری را انجام دهید به طور قطع الان زمان مناسبی برای کد نویسی آن نیست!
6- کد من کامپایل میشود!
باید دقت داشت که کامپایلر فقط syntax کدهای شما را بررسی کرده و خطاهای آنرا گوشزد میکند و نه نحوهی عملکرد آنرا.
7- من برای نوشتن آزمایشات حقوق نمیگیرم!
باید اذعان داشت که به شما جهت صرف تمام وقت یک روز خود برای دیباگ کردن یک خطا هم حقوق نمیدهند! شما برای تهیه یک کد قابل قبول و قابل اجرا حقوق میگیرید و آزمایش واحد نیز ابزاری است جهت نیل به این مقصود (همانند یک IDE و یا یک کامپایلر).
8- احساس گناه خواهم کرد اگر تیم فنی کنترل کیفیت و آزمایشات را از کار بی کار کنم!!
نگران نباشید، این اتفاق نخواهد افتاد! بحث ما در اینجا آزمایش کوچکترین اجزا و واحدهای یک سیستم است. موارد دیگری مانند functional testing, acceptance testing, performance & environmental testing, validation & verification, formal analysis توسط تیمهای کنترل کیفیت و آزمایشات هنوز باید بررسی شوند.
9- شرکت من اجازه اجرای آزمایشات واحد را بر روی سیستمهای در حال اجرا نمیدهد.
قرار هم نیست بدهد! چون دیگر نام آن آزمایش واحد نخواهد بود. این آزمایشات باید بر روی سیستم شما و توسط ابزار و امکانات شما صورت گیرد.
پ.ن.
در هشتمین دلیل ذکر شده، از acceptance testing نامبرده شده. تفاوت آن با unit testing به صورت زیر است:
آزمایش واحد:
توسط برنامه نویسها تعریف میشود
سبب اطمینان خاطر برنامه نویسها خواهد شد
واحدهای کوچک سیستم را مورد بررسی قرار میدهد
یک آزمایش سطح پائین ( low level ) به شمار میرود
بسیار سریع اجرا میشود
به صورت خودکار (100 درصد خودکار است) و با برنامه نویسی قابل کنترل است
اما در مقابل آزمایش پذیرش به صورت زیر است:
توسط مصرف کنندگان تعریف میشود
سبب اطمینان خاطر مصرف کنندگان میشود.
کل برنامه مورد آزمایش قرار میگیرد
یک آزمایش سطح بالا ( high level ) به شمار میرود
ممکن است طولانی باشد
عموما به صورت دستی یا توسط یک سری اسکریپت اجرا میشود
مثال : گزارش ماهیانه باید جمع صحیحی از تمام صفحات را در آخرین برگه گزارش به همراه داشته باشد
ادامه دارد...
SortedSet قرار گرفته در فضای نام System.Collections.Generic دات نت 4، لیستی از اشیاء به صورت خودکار مرتب شده را ارائه میدهد. SortedSet نیز همانند HashSet از اعضای منحصربفردی تشکیل خواهد شد اما اینبار به شکلی مرتب شده. برای پیاده سازی آن از red-black tree data structure استفاده شده است که مهمترین مزیت آن امکان افزودن و یا حذف اشیاء به آن بدون کاهش قابل توجه کارآیی برنامه است.
مثال اول:
using System;
using System.Collections.Generic;
namespace SortedSetTest
{
class Program
{
static void sample1()
{
var setRange = new SortedSet<int> { 2, 5, 6, 2, 1, 4, 8 };
foreach (var i in setRange)
{
Console.WriteLine(i);
}
}
static void Main()
{
sample1();
}
}
}
مثال دوم:
using System;
using System.Collections.Generic;
namespace SortedSetTest
{
class Program
{
static void sample2()
{
var setRange = new SortedSet<int>();
var random = new Random();
for (int counter = 0; counter < 100; counter++)
{
var rnd = random.Next(-180, 181);
if (!setRange.Add(rnd))
{
Console.WriteLine("Couldn't add {0}", rnd);
}
}
Console.WriteLine("Result set:");
foreach (var item in setRange)
{
Console.WriteLine(item);
}
}
static void Main()
{
sample2();
}
}
}
مثال سوم:
اگر از سایر انواع سفارشی تعریف شده استفاده نمائید، باید روش مقایسهی آنها را نیز با پیاده سازی اینترفیس استاندارد IComparable ارائه دهید؛ در غیر اینصورت با خطای At least one object must implement IComparable متوقف خواهید شد.
using System;
using System.Collections;
using System.Collections.Generic;
namespace SortedSetTest
{
class FileInfo
{
public string Name { set; get; }
public long Size { set; get; }
}
class FileInfoComparer : IComparer<FileInfo>
{
public int Compare(FileInfo x, FileInfo y)
{
var caseiComp = new CaseInsensitiveComparer();
return caseiComp.Compare(x.Name, y.Name);
}
}
class Program
{
static void sample3()
{
var setRange = new SortedSet<FileInfo>(new FileInfoComparer())
{
new FileInfo
{
Name = "file1.txt",
Size = 100
},
new FileInfo
{
Name = "file2.txt",
Size = 10
},
new FileInfo
{
Name = "file3.txt",
Size = 300
}
};
foreach (var item in setRange)
{
Console.WriteLine(item.Name);
}
}
static void Main()
{
sample3();
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
در این مثال اشیایی از نوع کلاس FileInfo به لیست ویژهی ما اضافه شدهاند. برای اینکه امکان مقایسهی آنها فراهم باشد ، کلاس FileInfoComparer با پیاده سازی اینترفیس IComparer ، روش مقایسه دو شیء از این دست را ارائه میدهد.
اشتراکها