將參照 DLL 併入單一 WPF 執行檔

將 .NET 執行檔跟所參照 DLL 合併成單一 EXE 檔的做法之前介紹過(Visual Studio編譯小技巧:工具程式一檔搞定 ),在專案用 NuGet 安裝 MSBuild.ILMerge.Task 就能輕鬆搞定。之前在 Console Application 用得挺順利,今天用在 WPF 卻卡在一個錯誤無法編譯:

The item "X:\TFS\RptBatchPrint\packages\Hardcodet.NotifyIcon.Wpf.1.0.8\lib\net451\Hardcodet.Wpf.TaskbarNotification.dll" in item list "ReferencePath" does not define a value for metadata "CopyLocal".  In order to use this metadata, either qualify it by specifying %(ReferencePath.CopyLocal), or ensure that all items in this list define a value for this metadata.  

用關鍵字爬文竟連回自己的文章,網友 agrozyme 留言提到相似問題,在 WPF 專案遇過一模一樣的錯誤稍後還留言分享了解決方法,但不幸地文件連結年久毁損,解法失傳…(搥牆)再爬文找到一兩則相似問題回報,但無人提出解決方案。

最後,換了關鍵字幸運找到另一種解法:Combining multiple assemblies into a single EXE for a WPF application – DigitallyCreated

原理是在 csproj 加入一段 AfterResolveReference 編譯作業指令(位置可放在 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>

這段設定會在編譯時將專案參照到的 DLL 轉為 EmbeddedResource,之就可發現 EXE 變肥許多,用 JustDecompile 反組譯可看到專案參照的 Nancy.Hosting.Self、Json.NET、NLog、Hardcodet.Wpf.TaskbarNotification 等 DLL 已被轉成 EXE 內嵌資源:

接著在專案新増一個 Program.cs 當作啟動物件,先註冊自訂 AppDomain.CurrentDomain.AssemblyResolve 事件再呼叫原本的 WPF Application 啟動方法(App.Main())。在 AssemblyResolve 事件中,依組件名稱從 EmbeddedResource 中取出組件 DLL,再以 Reflection 方式載入傳回:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
 
namespace RptBatchPrintAgent
{
    public class Program
    {
        [STAThreadAttribute]
        public static void Main()
        {
            AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
            App.Main();
        }
 
        private static Assembly OnResolveAssembly(object sender, ResolveEventArgs args)
        {
            Assembly executingAssembly = Assembly.GetExecutingAssembly();
            AssemblyName assemblyName = new AssemblyName(args.Name);
 
            string path = assemblyName.Name + ".dll";
            if (assemblyName.CultureInfo.Equals(CultureInfo.InvariantCulture) == false)
            {
                path = String.Format(@"{0}\{1}", assemblyName.CultureInfo, path);
            }
 
            using (Stream stream = executingAssembly.GetManifestResourceStream(path))
            {
                if (stream == null)
                    return null;
 
                byte[] assemblyRawBytes = new byte[stream.Length];
                stream.Read(assemblyRawBytes, 0, assemblyRawBytes.Length);
                return Assembly.Load(assemblyRawBytes);
            }
        }
    }
}

最後修改 Startup Object 指向 Program,大功告成!

就這樣,在 ILMerge 之外又學會一招  DLL 合併技巧~

註:原始文章下方有一些讀者回應提及這個做法在某些情境可能遇到的問題,如遇狀況可供參考。

歡迎推文分享:
Published 13 June 2017 11:33 PM 由 Jeffrey
Filed under:
Views: 1,595



意見

沒有意見

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<June 2017>
SunMonTueWedThuFriSat
28293031123
45678910
11121314151617
18192021222324
2526272829301
2345678
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication