مطالب
Roslyn #5
بررسی Semantic Models

همانطور که از قسمت قبل به‌خاطر دارید، برای دسترسی به اطلاعات semantics، نیاز به یک context مناسب که همان Compilation API است، می‌باشد. این context دارای اطلاعاتی مانند دسترسی به تمام نوع‌های تعریف شده‌ی توسط کاربر و متادیتاهای ارجاعی، مانند کلاس‌های پایه‌ی دات نت فریم‌ورک است. بنابراین پس از ایجاد وهله‌ای از Compilation API، کار با فراخوانی متد GetSemanticModel آن ادامه می‌یابد. در ادامه با مثال‌هایی، کاربرد این متد را بررسی خواهیم کرد.


ساختار جدید Optional

خروجی‌های تعدادی از متدهای Roslyn با ساختار جدیدی به نام Optional ارائه می‌شوند:
    public struct Optional<T>
    {
        public bool HasValue { get; }
        public T Value { get; }
    }
این ساختار که بسیار شبیه است به ساختار قدیمی <Nullable<T، منحصر به Value types نیست و Reference types را نیز شامل می‌شود و بیانگر این است که آیا یک Reference type، واقعا مقدار دهی شده‌است یا خیر؟


دریافت مقادیر ثابت Literals

فرض کنید می‌خواهیم مقدار ثابت ; int x = 42 را دریافت کنیم. برای اینکار ابتدا باید syntax tree آن تشکیل شود و سپس نیاز به یک سری حلقه و if و else و همچنین بررسی نال بودن بسیاری از موارد است تا به نود مقدار ثابت 42 برسیم. سپس متد GetConstantValue مربوط به GetSemanticModel را بر روی آن فراخوانی می‌کنیم تا به مقدار واقعی آن که ممکن است در اثر محاسبات جاری تغییر کرده باشد، برسیم.
اما روش بهتر و توصیه شده، استفاده از CSharpSyntaxWalker است که در انتهای قسمت سوم معرفی شد:
class ConsoleWriteLineWalker : CSharpSyntaxWalker
{
    public ConsoleWriteLineWalker()
    {
        Arguments = new List<ExpressionSyntax>();
    }
 
    public List<ExpressionSyntax> Arguments { get; }
 
    public override void VisitInvocationExpression(InvocationExpressionSyntax node)
    {
        var member = node.Expression as MemberAccessExpressionSyntax;
        var type = member?.Expression as IdentifierNameSyntax;
        if (type != null && type.Identifier.Text == "Console" && member.Name.Identifier.Text == "WriteLine")
        {
            if (node.ArgumentList.Arguments.Count == 1)
            {
                var arg = node.ArgumentList.Arguments.Single().Expression;
                Arguments.Add(arg);
                return;
            }
        }
 
        base.VisitInvocationExpression(node);
    }
}
اگر به کدهای ادامه‌ی بحث دقت کنید، قصد داریم مقادیر ثابت آرگومان‌های Console.WriteLine را استخراج کنیم. به همین جهت در این SyntaxWalker، نوع Console و متد WriteLine آن مورد بررسی قرار گرفته‌اند. اگر این نود دارای یک تک آرگومان بود، آین آرگومان استخراج شده و به لیست آرگومان‌های خروجی این کلاس اضافه می‌شود.
در ادامه نحوه‌ی استفاده‌ی از این SyntaxWalker را ملاحظه می‌کنید. در اینجا ابتدا سورس کدی حاوی یک سری Console.WriteLine که دارای تک آرگومان‌های ثابتی هستند، تبدیل به syntax tree می‌شود. سپس از روی آن CSharpCompilation تولید می‌گردد تا بتوان به اطلاعات semantics دسترسی یافت:
static void getConstantValue()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        Console.WriteLine(3.14);
                        Console.WriteLine(""qux"");
                        Console.WriteLine('c');
                        Console.WriteLine(null);
                        Console.WriteLine(x * 2 + 1);
                    }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
 
    // Analyze the constant argument (if any).
    foreach (var arg in walker.Arguments)
    {
        var val = model.GetConstantValue(arg);
        if (val.HasValue)
        {
            Console.WriteLine(arg + " has constant value " + (val.Value ?? "null") + " of type " + (val.Value?.GetType() ?? typeof(object)));
        }
        else
        {
            Console.WriteLine(arg + " has no constant value");
        }
    }
}
در ادامه با استفاده از CSharpCompilation و متد GetSemanticModel آن به SemanticModel جاری دسترسی خواهیم یافت. اکنون SyntaxWalker را وارد به حرکت بر روی ریشه‌ی syntax tree سورس کد آنالیز شده می‌کنیم. به این ترتیب لیست آرگومان‌های متدهای Console.WriteLine بدست می‌آیند. سپس با فراخوانی متد model.GetConstantValue بر روی هر آرگومان دریافتی، مقادیر آن‌ها با فرمت <Optional<T استخراج می‌شوند.
خروجی نمایش داده شده‌ی توسط برنامه به صورت ذیل است:
 3.14 has constant value 3.14 of type System.Double
"qux" has constant value qux of type System.String
'c' has constant value c of type System.Char
null has constant value null of type System.Object
x * 2 + 1 has no constant value


درک مفهوم Symbols

اینترفیس ISymbol در Roslyn، ریشه‌ی تمام Symbolهای مختلف مدل سازی شده‌ی در آن است که تعدادی از آن‌ها را در تصویر ذیل مشاهده می‌کنید:


API کار با Symbols بسیار شبیه به API کار با Reflection است با این تفاوت که در زمان آنالیز کدها رخ می‌دهد و نه در زمان اجرای برنامه. همچنین در Symbols API امکان دسترسی به اطلاعاتی مانند locals, labels و امثال آن نیز وجود دارد که با استفاده از Reflection زمان اجرای برنامه قابل دسترسی نیستند. برای مثال فضاهای نام در Reflection صرفا به صورت رشته‌ای، با دات جدا شده از نوع‌های آنالیز شده‌ی توسط آن است؛ اما در اینجا مطابق تصویر فوق، یک اینترفیس مجزای خاص خود را دارد. جهت سهولت کار کردن با Symbols، الگوی Visitor با معرفی کلاس پایه‌ی SymbolVisitor نیز پیش بینی شده‌است.
static void workingWithSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    void Bar(int x)
                    {
                        // #insideBar
                    }
                }
 
                class Qux
                {
                    protected int Baz { get; set; }
                }
                ";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse enclosing symbol hierarchy.
    var cursor = code.IndexOf("#insideBar");
    var barSymbol = model.GetEnclosingSymbol(cursor);
    for (var symbol = barSymbol; symbol != null; symbol = symbol.ContainingSymbol)
    {
        Console.WriteLine(symbol);
    }
 
    // Analyze accessibility of Baz inside Bar.
    var bazProp = ((CompilationUnitSyntax)root)
        .Members.OfType<ClassDeclarationSyntax>()
        .Single(m => m.Identifier.Text == "Qux")
        .Members.OfType<PropertyDeclarationSyntax>()
        .Single();
    var bazSymbol = model.GetDeclaredSymbol(bazProp);
    var canAccess = model.IsAccessible(cursor, bazSymbol);
}
یکی از کاربردهای مهم Symbols API دریافت اطلاعات Symbols نقطه‌ای خاص از کدها می‌باشد. برای مثال در محل اشاره‌گر ادیتور، چه Symbols ایی تعریف شده‌اند و از آن‌ها در مباحث ساخت افزونه‌های آنالیز کدها زیاد استفاده می‌شود. نمونه‌ای از آن‌را در قطعه کد فوق ملاحظه می‌کنید. در اینجا با استفاده از متد GetEnclosingSymbol، سعی در یافتن Symbols قرار گرفته‌ی در ناحیه‌ی insideBar# کدهای فوق داریم؛ با خروجی ذیل که نام demo.exe آن از نام CSharpCompilation آن گرفته شده‌است:
 Foo.Bar(int)
Foo
<global namespace>
Demo.exe
Demo, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null


همچنین در ادامه‌ی کد، توسط متد IsAccessible قصد داریم بررسی کنیم آیا Symbol قرار گرفته در محل کرسر، دسترسی به خاصیت protected کلاس Qux را دارد یا خیر؟ که پاسخ آن خیر است.


آشنایی با Binding symbols

یکی از مراحل کامپایل کد، binding نام دارد و در این مرحله است که اطلاعات Symbolic هر نود از Syntax tree دریافت می‌شود. برای مثال در اینجا مشخص می‌شود که این x، آیا یک متغیر محلی است، یا یک فیلد و یا یک خاصیت؟
مثال ذیل بسیار شبیه است به مثال getConstantValue ابتدای بحث، با این تفاوت که در حلقه‌ی آخر کار از متد GetSymbolInfo استفاده شده‌است:
static void bindingSymbols()
{
    // Get the syntax tree.
    var code = @"
                using System;
 
                class Foo
                {
                    private int y;
 
                    void Bar(int x)
                    {
                        Console.WriteLine(x);
                        Console.WriteLine(y);
 
                        int z = 42;
                        Console.WriteLine(z);
 
                        Console.WriteLine(a);
                    }
                }";
 
    var tree = CSharpSyntaxTree.ParseText(code);
    var root = tree.GetRoot();
 
    // Get the semantic model from the compilation.
    var mscorlib = MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
    var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib);
    var model = comp.GetSemanticModel(tree);
 
    // Traverse the tree.
    var walker = new ConsoleWriteLineWalker();
    walker.Visit(root);
 
    // Bind the arguments.
    foreach (var arg in walker.Arguments)
    {
        var symbol = model.GetSymbolInfo(arg);
        if (symbol.Symbol != null)
        {
            Console.WriteLine(arg + " is bound to " + symbol.Symbol + " of type " + symbol.Symbol.Kind);
        }
        else
        {
            Console.WriteLine(arg + " could not be bound");
        }
    }
}
با این خروجی:
 x is bound to int of type Parameter
y is bound to Foo.y of type Field
z is bound to z of type Local
a could not be bound
در مثال فوق، با استفاده از Syntax Walker طراحی شده در ابتدای بحث که کار استخراج آرگومان‌های متدهای Console.WriteLine را انجام می‌دهد، قصد داریم بررسی کنیم، هر آرگومان به چه Symbol ایی بایند شده‌است و نوعش چیست؟ برای مثال Console.WriteLine اول که از پارامتر x استفاده می‌کند، نوع x مورد استفاده‌اش چیست؟ آیا فیلد است، متغیر محلی است یا یک پارامتر؟ این اطلاعات را با استفاده از متد model.GetSymbolInfo می‌توان استخراج کرد.
اشتراک‌ها
فونت‌های نامرئی در طراحی وب
یکی از ایده‌های جدید در طراحی وب، استفاده از فونت‌های نامرئی است. یک فونت نامرئی فونتی است که تمام نویسه‌های آن خالی هستند و متن‌های نوشته شده با آن هیچ فضایی اشغال نمی‌کنند. 
فونت‌های نامرئی در طراحی وب
نظرات مطالب
EF Code First #12
Html.Action‌ها برخلاف تصور، یک چرخه‌ی کامل ASP.NET MVC را از صفر آغاز می‌کنند و از چرخه‌ی موجود استفاده نمی‌کنند.
نظرات مطالب
آزمایش Web APIs توسط Postman - قسمت چهارم - نوشتن آزمون‌ها و اسکریپت‌ها
یک نکته‌ی تکمیلی: پردازش سایر فرمت‌های دریافتی از سرور
در این مطلب نحوه‌ی تبدیل response دریافتی را به json بررسی کردیم. سایر حالات زیر نیز توسط postman پشتیبانی می‌شوند:
عملکرد
 متد
 تبدیل XML به شیء JSON
let jsonObject = xml2Json(responseBody);
 تبدیل JSON به شیء JSON
let jsonData = pm.response.json();
 تبدیل بدنه‌ی response به متن ساده
let text = pm.response.text();
 تبدیل HTML بدنه‌ی response به یک شیء از نوع cheerio؛ مانند:
let titleText = html.find('h1').text();
let html = cheerio(responseBody);
مطالب
انتخاب layoutهای متفاوت در برنامه‌های Angular
شاید برای شما هم پیش آمده باشد که در یک برنامه‌ی Angular بخواهید layoutهای مختلفی داشته باشید؛ مثلا هنگام لاگین، طبق عرف کار باید هدر و فوتر صفحه از بین بروند و فقط فرم لاگین نمایش داده شود و یا بخواهید هنگام لاگین، یک layout مخصوص پنل مدیریتی داشته باشید و یا …

قبل از شروع، فرض را بر آن می‌گیریم که حداقل نیاز‌های یک پروژه‌ی Angular را آماده کرده اید. سپس یک پوشه‌ی جدید را به نام layout می‌سازیم و layout‌های مربوطه را در آن ایجاد میکنیم. با دستور زیر یک کامپوننت جدید را که layout ما خواهد شد، با نام دلخواهی ایجاد می‌کنیم:
ng g c Loginlayout
 و همچنین یک کامپوننت دیگر را برای صفحه‌ی اصلی به نام homelayout می‌سازیم:
ng g c homelayout

در ادامه Loginlayout را باز کرده و تنظیمات زیر را لحاظ کنید:
<div style="width: 100%;height: 250px;background-color: aquamarine">
   <h1>Header</h1>
</div>

<router-outlet></router-outlet>

<div style="width: 100%;height: 250px;background-color: brown">
  <h1>Foother</h1>
</div>
در اینجا یک هدر و یک فوتر را ساخته و <router-outlet></router-outlet> را در آن قرار می‌دهیم که قسمت پویای ما خواهد شد.

اکنون وارد کامپوننت home layout شوید و دقیقا مانند قبل، تنظیمات دلخواه خود را انجام داده و همچنین <router-outlet></router-outlet> راهم درون جائیکه می‌خواهید به صورت پویا باشد بگذارید.
تا اینجا ما فقط layoutها را طراحی کردیم. در ادامه در ریشه‌ی پروژه، سه کامپوننت را به نام‌های Home , Login, About میسازیم. Home و About دارای یک قالب و Login هم داری قالب مخصوص به خود میباشد.

سپس وارد کامپوننت آغازین برنامه (app.component.html) شوید و در آن <router-outlet></router-outlet> را وارد کنید. در اینجا دیگر نیازی به نوشتن تگ‌های خاص دیگری نیست.

در ادامه به اصلی‌ترین قسمت، یعنی مسیریابی می‌رسیم. وارد app.module.ts شوید و آن را به صورت زیر تنظیم کنید:
export const routes: Routes = [    
           { 
                path: 'Loginlayout', 
                component: LoginlayoutComponent ,
                children: [
                  { path: 'Login', component: LoginComponent, pathMatch: 'full'}                 
                ]
            },
            { 
                path: 'Homelayout', 
                component: HomelayoutComponent,
                children: [
                  { path: 'About', component: AbouComponent, pathMatch: 'full'},
                  { path: 'Home', component: HomeComponent, pathMatch: 'full'}
                ]
            }          
];
همانطورکه ملاحظه می‌کنید، مسیریابی بالا شامل مسیریابی‌های تو در تویی است. در اینجا کامپوننت‌های Home و About درون HomelayoutComponent بارگذاری می‌شوند و خود HomelayoutComponent  نیز درون app.component.
همچنین برای اینکه مشخص شود کدام کامپوننت به عنوان کامپوننت پیشفرض نمایش داده شود، به صورت زیر عمل میکنیم:
path: '', 
component: HomelayoutComponent,
children: [
  { path: '', component:HomeComponent, pathMatch: 'full'}         
]
به  این روش میتوانید هر تعداد layout ایی را که میخواهید، ایجاد کنید.

کدهای کامل این مطلب را می‌توانید از اینجا دریافت و یا به صورت آنی آزمایش کنید.
مطالب
Angular CLI - قسمت چهارم - تنظیمات مسیریابی
«انجام تنظیمات مسیریابی پیش فرض پروژه جدید توسط Angular CLI» را در قسمت دوم این سری بررسی کردیم. در ادامه با قابلیت‌های بیشتری از امکانات تنظیمات مسیریابی موجود در Angular CLI، آشنا خواهیم شد.

یک مثال: در ادامه یک پروژه‌ی جدید مبتنی بر Angular CLI را به همراه تنظیمات ابتدایی مسیریابی آن ایجاد می‌کنیم:
> ng new angular-routing --routing
همانطور که در قسمت دوم نیز ذکر شد، پرچم routing در اینجا، سبب ایجاد فایل app-routing.module.ts نیز خواهد گردید:


 و تنظیمات مرتبط با آن به صورت خودکار به قسمت imports فایل app.module.ts اضافه می‌شوند و آماده‌ی استفاده هستند.
همچنین اگر به فایل src\app\app.component.html مراجعه کنیم، router-outlet نیز به آن افزوده شده‌است و مدیریت نمایش مسیریابی‌ها در این قسمت انجام خواهد شد.

در ادامه‌ی این مثال، دو کامپوننت جدید را به نام‌های dashboard و customer ایجاد می‌کنیم:
>ng g c dashboard
>ng g c customer


هدف این است که مسیریابی‌هایی را جهت کار و نمایش این کامپوننت‌ها ایجاد کنیم. به همین جهت به فایل src\app\app-routing.module.ts مراجعه کرده و تغییرات ذیل را اعمال کنید:
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { DashboardComponent } from './dashboard/dashboard.component';
import { CustomerComponent } from './customer/customer.component';

const routes: Routes = [
  { path: '', pathMatch: 'full', redirectTo: 'dashboard' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'customer', component: CustomerComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
در اینجا ابتدا کامپوننت‌های جدید، import شده و سپس یک مسیریابی پیش فرض به کامپوننت dashboard و دو مسیریابی جدید دیگر به کامپوننت‌های dashboard و customer ایجاد شده‌اند.
البته باید دقت داشت که چون پیشتر با اجرای دستورات ng g c، این کامپوننت‌ها به صورت خودکار به تعاریف فایل app.module.ts اضافه شده‌اند، امکان استفاده‌ی از آن‌ها در اینجا میسر است:
@NgModule({
  declarations: [
    AppComponent,
    DashboardComponent,
    CustomerComponent
  ],

پس از تعریف مسیریابی‌ها، به فایل src\app\app.component.html مراجعه کرده و لینک‌هایی را به این مسیریابی‌های جدید ایجاد می‌کنیم:
<h1>
  {{title}}
</h1>
<nav>
  <ul>
    <li><a href="" [routerLink]="['/dashboard']">Dashboard</a></li>
    <li><a href="" [routerLink]="['/customer']">Customer</a></li>
  </ul>
</nav>
<router-outlet></router-outlet>

اکنون اگر دستور کامپایل و گشودن برنامه را در مرورگر پیش فرض سیستم صادر کنیم:
> ng serve -o
یک چنین تصویری حاصل خواهد شد:


با توجه به تنظیمات مسیریابی پیش فرض انجام شده، ابتدا مسیر http://localhost:4200/dashboard بارگذاری شده‌است.


ایجاد ماژول‌های جدید به همراه تنظیمات مسیریابی آن‌ها

در قسمت قبل با نحوه‌ی ایجاد ماژول‌های جدید توسط Angular CLI آشنا شدیم:
> ng g module admin
این فرمان، فایل admin.module.ts را تولید می‌کند. در اینجا می‌توان پرچم مسیریابی را نیز ذکر کرد (برای اینکار یک پنجره‌ی خط فرمان دیگر را باز کنید و اجازه دهید تا پنجره‌ی خط فرمان ng serve -o باز باقی بماند و مدام مشغول بررسی تغییرات و کامپایل پشت صحنه‌ی کار باشد):
> ng g m admin --routing
در این حالت دو فایل admin.module.ts و همچنین admin-routing.module.ts تولید می‌شوند.


سپس داخل این ماژول یک کامپوننت جدید را به نام admin ایجاد می‌کنیم:
> ng g c admin
در اینجا چون این کامپوننت، هم نام پوشه‌ی admin است، داخل همان پوشه ایجاد خواهد شد.
برای مثال اگر نیاز به ایجاد کامپوننت دیگری به نام emails داخل این پوشه بود، باید به نحو ذیل عمل کرد:
> ng g c admin/email
installing component
  create src\app\admin\email\email.component.css
  create src\app\admin\email\email.component.html
  create src\app\admin\email\email.component.spec.ts
  create src\app\admin\email\email.component.ts
  update src\app\admin\admin.module.ts
در این حالت پوشه‌ی جدید email داخل پوشه‌ی admin ایجاد شده و فایل‌های کامپوننت جدید email به آن اضافه می‌شوند. همچنین اگر دقت کنید، اینبار سطر update آخری، فایل admin.module.ts را به روز رسانی کرده‌است و در قسمت declarations آن، دو کامپوننت ایجاد شده را تعریف کرده‌است:
@NgModule({
  imports: [
    CommonModule,
    AdminRoutingModule
  ],
  declarations: [AdminComponent, EmailComponent]
})
export class AdminModule { }

تا اینجا ماژول جدید admin را ایجاد کرده‌ایم؛ اما برنامه‌ی اصلی از آن اطلاعی ندارد. به همین جهت به فایل src\app\app.module.ts مراجعه کرده و این ماژول جدید را به آن معرفی می‌کنیم:
import { AdminModule } from './admin/admin.module';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
 
    AdminModule,

    AppRoutingModule
  ],
ابتدا کلاس این ماژول import شده و سپس آن‌را پیش از AppRoutingModule تعریف می‌کنیم.

در ادامه برای تعریف مسیریابی‌های این ماژول جدید، به فایل src\app\admin\admin-routing.module.ts آن مراجعه کرده و ثابت routes آن‌را مقدار دهی می‌کنیم:
import { AdminComponent } from './admin.component';
import { EmailComponent } from './email/email.component';

const routes: Routes = [
    { 
      path: 'admin', 
      component: AdminComponent,
      children:[
        { path:'', component:EmailComponent },
        { path:'email', component:EmailComponent }
      ]
    }
];
در اینجا مسیریابی admin، دارای فرزند email نیز می‌باشد و پیش فرض آن نیز به email تنظیم شده‌است.
سپس به فایل app\admin\admin.component.html نیز مراجعه کرده و router-outlet آن‌را به آن اضافه می‌کنیم:
<p>
  admin works!
</p>
<router-outlet></router-outlet>
تا اینجا هرچند لینک جدیدی را به ناحیه‌ی ادمین تعریف نکرده‌ایم، اما مسیریابی تعریف شده‌ی آن کار می‌کند:


یک نکته: امکان تولید route guards نیز توسط Angular CLI برای محافظت از مسیریابی خاصی وجود دارد. برای این منظور می‌توان دستور ذیل را صادر کرد:
>ng g guard auth
که سبب تولید فایل auth.guard.ts می‌شود.
مطالب
یکی کردن اسمبلی‌های یک پروژه‌ی WPF
فرض کنید پروژه‌ی WPF شما از چندین پروژه‌ی ‍Class library و اسمبلی‌های جانبی دیگر، تشکیل شده‌است. اکنون نیاز است جهت سهولت توزیع آن، تمام این فایل‌ها را با هم یکی کرده و تبدیل به یک فایل EXE نهایی کنیم. مایکروسافت ابزاری را به نام ILMerge، برای یک چنین کارهایی تدارک دیده‌است؛ اما این برنامه با WPF سازگار نیست. در ادامه قصد داریم اسمبلی‌های جانبی را تبدیل به منابع مدفون شده در فایل EXE برنامه کرده و سپس آن‌ها را در اولین بار اجرای برنامه، به صورت خودکار بارگذاری و در برنامه مورد استفاده قرار دهیم.

یک مثال جهت بازتولید کدهای این مطلب
الف) یک پروژه‌ی WPF جدید را به نام MergeAssembliesIntoWPF ایجاد کنید.
ب) یک پروژه‌ی Class library جدید را به نام MergeAssembliesIntoWPF.ViewModels به این Solution اضافه کنید. از آن برای تعریف ViewModelهای برنامه استفاده خواهیم کرد.
برای نمونه کلاس ذیل را به آن اضافه کنید:
namespace MergeAssembliesIntoWPF.ViewModels
{
    public class ViewModel1
    {
        public string Data { set; get; }

        public ViewModel1()
        {
            Data = "Test";
        }
    }
}
ج) یک پروژه‌ی WPF User control library را نیز به نام MergeAssembliesIntoWPF.Shell به این Solution اضافه کنید. از آن برای تعریف Viewهای برنامه کمک خواهیم گرفت.
به این پروژه ارجاعی را به اسمبلی قسمت (ب) اضافه نموده و برای نمونه User control ذیل را به نام View1.xaml به آن اضافه نمائید:
<UserControl x:Class="MergeAssembliesIntoWPF.Shell.View1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             xmlns:VM="clr-namespace:MergeAssembliesIntoWPF.ViewModels;assembly=MergeAssembliesIntoWPF.ViewModels"
             d:DesignHeight="300" d:DesignWidth="300">
    <UserControl.Resources>
        <VM:ViewModel1 x:Key="ViewModel1" />
    </UserControl.Resources>
    <Grid DataContext="{Binding Source={StaticResource ViewModel1}}">
        <TextBlock Text="{Binding Data}" />
    </Grid>
</UserControl>
در پروژه اصلی Solution (قسمت الف)، ارجاعاتی را به دو اسمبلی قسمت‌های ب و ج اضافه کنید. سپس MainWindow.xaml آن‌را به نحو ذیل تغییر داده و برنامه را اجرا کنید:
<Window x:Class="MergeAssembliesIntoWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:V="clr-namespace:MergeAssembliesIntoWPF.Shell;assembly=MergeAssembliesIntoWPF.Shell"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <V:View1 x:Key="View1" />
    </Window.Resources>
    <Grid>
        <V:View1 />
    </Grid>
</Window>
تا اینجا باید متن Test در پنجره اصلی برنامه ظاهر شود.


ب) مدفون کردن خودکار اسمبلی‌های جانبی برنامه در فایل EXE آن
فایل csproj پروژه اصلی را خارج از VS.NET باز کنید. در انتهای آن سطر ذیل قابل مشاهده است:
 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
پس از این سطر، چند سطر ذیل را اضافه کنید:
  <Target Name="AfterResolveReferences">
    <ItemGroup>
      <EmbeddedResource Include="@(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
        <LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
      </EmbeddedResource>
    </ItemGroup>
  </Target>
این task جدید MSBuild سبب خواهد شد تا با هر بار Build برنامه، اسمبلی‌هایی که در ارجاعات برنامه دارای خاصیت Copy local مساوی true هستند، به صورت خودکار به صورت یک resource جدید در فایل exe برنامه مدفون شوند. عموما ارجاعاتی که دستی اضافه می‌شوند، مانند دو اسمبلی یاد شده در ابتدای بحث، دارای خاصیت Copy local=true نیز هستند.
پس از این تغییر نیاز است یکبار پروژه را بسته و مجددا باز کنید. اکنون پروژه را build کنید و جهت اطمینان بیشتر آن‌را برای مثال توسط ILSpy مورد بررسی قرار دهید:


همانطور که مشاهده می‌کنید، دو اسمبلی مورد استفاده در برنامه به صورت خودکار در قسمت منابع فایل EXE مدفون شده‌اند.
اگر به مسیر LogicalName تنظیمات فوق دقت کنید، DestinationSubDirectory نیز ذکر شده‌است. علت این است که بسیاری از اسمبلی‌های بومی سازی شده WPF با نام‌هایی یکسان اما در پوشه‌هایی مانند fa، fr و امثال آن ذخیره می‌شوند. به همین جهت نیاز است بین این‌ها تمایز قائل شد.


ج) بارگذاری خودکار اسمبلی‌ها در AppDomain برنامه

تا اینجا اسمبلی‌های جانبی را در فایل EXE مدفون کرده‌ایم. اکنون نوبت به بارگذاری آن‌ها در AppDomain برنامه است. برای اینکار نیاز است تا روال رخدادگردان AppDomain.CurrentDomain.AssemblyResolve را تحت نظر قرار داده و اسمبلی‌هایی را که برنامه درخواست می‌کند، در همینجا از منابع خوانده و به AppDomain اضافه کرد.
انجام اینکار در برنامه‌های WinForms ساده‌است. فقط کافی است به متد Program.Main برنامه مراجعه کرده و تعریف یاد شده را به ابتدای متد Main اضافه کرد. اما در WPF هرچند فایل App.xaml.cs به نظر نقطه‌ی آغازین برنامه است، اما در واقع اینطور نیست. برای نمونه، پوشه‌ی obj\Debug برنامه را گشوده و فایل App.g.i.cs آن‌را بررسی کنید. در اینجا می‌توانید همان رویه شبیه به برنامه‌های WinForm را در متد Program.Main آن، مشاهده کنید. بنابراین نیاز است کنترل این مساله را راسا در دست بگیریم:
using System;
using System.Globalization;
using System.Reflection;

namespace MergeAssembliesIntoWPF
{
    public class Program
    {
        [STAThreadAttribute]
        public static void Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
            App.Main();
        }

        private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            var executingAssembly = Assembly.GetExecutingAssembly();
            var assemblyName = new AssemblyName(args.Name);

            var path = assemblyName.Name + ".dll";
            if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }

            using (var stream = executingAssembly.GetManifestResourceStream(path))
            {
                if (stream == null)
                    return null;

                var assemblyRawBytes = new byte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                return Assembly.Load(assemblyRawBytes);
            }
        }
    }
}
کلاس Program را با تعاریف فوق به پروژه خود اضافه نمائید. در اینجا Program.Main مورد نیاز خود را تدارک دیده‌ایم. کار آن مدیریت روال رخدادگردان AppDomain.CurrentDomain.AssemblyResolve برنامه پیش از شروع به هر کاری است. در روال رخداد گردان OnResolveAssembly، برنامه اعلام می‌کند که به چه اسمبلی خاصی نیاز دارد. ما آن‌را از قسمت منابع خوانده و سپس توسط متد Assembly.Load آن‌را در AppDomain برنامه بارگذاری می‌کنیم.
پس از اینکه کلاس فوق را اضافه کردید، نیاز است کلاس Program اضافه شده را به عنوان Startup object برنامه نیز معرفی کنید:

انجام اینکار ضروری است؛ در غیراینصورت با متد Main موجود در فایل App.g.i.cs تداخل می‌کند.
اکنون برای آزمایش برنامه، یکبار آن‌را Build کرده و بجز فایل Exe، مابقی فایل‌های موجود در پوشه‌ی bin را حذف کنید. سپس برنامه را خارج از VS.NET اجرا کنید. کار می‌کند!
MergeAssembliesIntoWPF.zip
 
نظرات مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
- زمانیکه پوشه‌های پروژه‌ها را جابجا می‌کنید، باید تمام فایل‌های csproj آن‌ها را باز کنید و سپس مسیرهای HintPath بسته‌های نیوگت را اصلاح کنید:
 <HintPath>..\..\..\packages\T4MVCExtensions.3.15.0\lib\net40\T4MVCExtensions.dll</HintPath>
اگر اینکار رخ ندهد، عملا کار بازیابی بسته‌ها پاسخ نخواهد داد چون HintPath‌های موجود به چند سطح بالاتر اشاره نمی‌کنند:
 <HintPath>..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll</HintPath>
- در پروژه‌ی RabbalShopCMS.DomainClasses شما به نظر یک سری کلاس‌ها نیستند و اضافه نشدند به سورس کنترل.
- قسمت post build event باید به صورت ذیل اصلاح شود:
 Copy "$(ProjectDir)$(OutDir)*.*" "$(SolutionDir)RabbalShopCMS.Web\bin\"
به این صورت تمام فایل‌های مرتبط کپی می‌شوند.
- در global.asax.cs پروژه‌ی اصلی باید این موارد را حذف کنید:
 ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine ());
Razor generator به ازای هر پلاگین دارای یک فایل RazorGeneratorMvcStart است که کارش ثبت یک ViewEngine مخصوص خواندن فایل‌های View از اسمبلی برنامه است که این موارد نباید حذف شوند و اگر حذف شوند، Viewهای پلاگین‌ها قابل مشاهده نخواهند بود.
- افزونه‌ی دارای Area نیازی نیست فایل layout داشته باشد. فقط باید دارای یک ViewStart باشد که به layout پروژه‌ی اصلی اشاره کند. این layout از پروژه‌ی پایه دریافت می‌شود و نه از افزونه. بنابراین فایل layout افزونه باید حذف شود و اضافی است.
- بعد در حالت solution چند پروژه‌ای اجرای دستور ذیل الزامی است: (خیلی مهم)
 PM> update-package
این مورد سبب خواهد شد تا تمام وابستگی‌های solution جاری به همراه تمام پروژه‌های مرتبط آن یکدست شوند.
- اگر با درخواست یک آدرس، فایل view پروژه‌ی دیگری بازگشت داده شد، ترتیب اضافه شدن PrecompiledMvcEngine را تغییر دهید. برای مثال در پروژه‌ی پلاگین:
 ViewEngines.Engines.Insert(0, engine);
در پروژه‌ی اصلی:
 ViewEngines.Engines.Add(engine);