فرض کنید پروژهی WPF شما از چندین پروژهی Class library و اسمبلیهای جانبی دیگر، تشکیل شدهاست. اکنون نیاز است جهت سهولت توزیع آن، تمام این فایلها را با هم یکی کرده و تبدیل به یک فایل EXE نهایی کنیم. مایکروسافت ابزاری را به نام ILMerge، برای یک چنین کارهایی تدارک دیدهاست؛ اما این برنامه با WPF سازگار نیست. در ادامه قصد داریم اسمبلیهای جانبی را تبدیل به منابع مدفون شده در فایل EXE برنامه کرده و سپس آنها را در اولین بار اجرای برنامه، به صورت خودکار بارگذاری و در برنامه مورد استفاده قرار دهیم.
یک مثال جهت بازتولید کدهای این مطلب
الف) یک پروژهی WPF جدید را به نام MergeAssembliesIntoWPF ایجاد کنید.
ب) یک پروژهی Class library جدید را به نام MergeAssembliesIntoWPF.ViewModels به این Solution اضافه کنید. از آن برای تعریف ViewModelهای برنامه استفاده خواهیم کرد.
برای نمونه کلاس ذیل را به آن اضافه کنید:
ج) یک پروژهی WPF User control library را نیز به نام MergeAssembliesIntoWPF.Shell به این Solution اضافه کنید. از آن برای تعریف Viewهای برنامه کمک خواهیم گرفت.
به این پروژه ارجاعی را به اسمبلی قسمت (ب) اضافه نموده و برای نمونه User control ذیل را به نام View1.xaml به آن اضافه نمائید:
در پروژه اصلی Solution (قسمت الف)، ارجاعاتی را به دو اسمبلی قسمتهای ب و ج اضافه کنید. سپس MainWindow.xaml آنرا به نحو ذیل تغییر داده و برنامه را اجرا کنید:
تا اینجا باید متن Test در پنجره اصلی برنامه ظاهر شود.
ب) مدفون کردن خودکار اسمبلیهای جانبی برنامه در فایل EXE آن
فایل csproj پروژه اصلی را خارج از VS.NET باز کنید. در انتهای آن سطر ذیل قابل مشاهده است:
پس از این سطر، چند سطر ذیل را اضافه کنید:
این 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 آن، مشاهده کنید. بنابراین نیاز است کنترل این مساله را راسا در دست بگیریم:
کلاس Program را با تعاریف فوق به پروژه خود اضافه نمائید. در اینجا Program.Main مورد نیاز خود را تدارک دیدهایم. کار آن مدیریت روال رخدادگردان AppDomain.CurrentDomain.AssemblyResolve برنامه پیش از شروع به هر کاری است. در روال رخداد گردان OnResolveAssembly، برنامه اعلام میکند که به چه اسمبلی خاصی نیاز دارد. ما آنرا از قسمت منابع خوانده و سپس توسط متد Assembly.Load آنرا در AppDomain برنامه بارگذاری میکنیم.
پس از اینکه کلاس فوق را اضافه کردید، نیاز است کلاس Program اضافه شده را به عنوان Startup object برنامه نیز معرفی کنید:
اکنون برای آزمایش برنامه، یکبار آنرا Build کرده و بجز فایل Exe، مابقی فایلهای موجود در پوشهی bin را حذف کنید. سپس برنامه را خارج از VS.NET اجرا کنید. کار میکند!
MergeAssembliesIntoWPF.zip
یک مثال جهت بازتولید کدهای این مطلب
الف) یک پروژهی WPF جدید را به نام MergeAssembliesIntoWPF ایجاد کنید.
ب) یک پروژهی Class library جدید را به نام MergeAssembliesIntoWPF.ViewModels به این Solution اضافه کنید. از آن برای تعریف ViewModelهای برنامه استفاده خواهیم کرد.
برای نمونه کلاس ذیل را به آن اضافه کنید:
namespace MergeAssembliesIntoWPF.ViewModels { public class ViewModel1 { public string Data { set; get; } public ViewModel1() { Data = "Test"; } } }
به این پروژه ارجاعی را به اسمبلی قسمت (ب) اضافه نموده و برای نمونه 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>
<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>
ب) مدفون کردن خودکار اسمبلیهای جانبی برنامه در فایل 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>
پس از این تغییر نیاز است یکبار پروژه را بسته و مجددا باز کنید. اکنون پروژه را 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 اضافه شده را به عنوان Startup object برنامه نیز معرفی کنید:
اکنون برای آزمایش برنامه، یکبار آنرا Build کرده و بجز فایل Exe، مابقی فایلهای موجود در پوشهی bin را حذف کنید. سپس برنامه را خارج از VS.NET اجرا کنید. کار میکند!
MergeAssembliesIntoWPF.zip