programing

단일 어셈블리 다국어 Windows Forms 배포 (ILMerge 및 위성 어셈블리 / 지역화)-가능합니까?

itsource 2021. 1. 16. 09:47
반응형

단일 어셈블리 다국어 Windows Forms 배포 (ILMerge 및 위성 어셈블리 / 지역화)-가능합니까?


Visual Studio 2008로 빌드 된 간단한 Windows Forms (C #, .NET 2.0) 응용 프로그램이 있습니다.

여러 UI 언어를 지원하고 양식의 "Localizable"속성과 문화 별 .resx 파일을 사용하여 지역화 측면이 원활하고 쉽게 작동합니다. Visual Studio는 문화 별 resx 파일을 위성 어셈블리로 자동 컴파일하므로 컴파일 된 응용 프로그램 폴더에는 이러한 위성 어셈블리를 포함하는 문화 별 하위 폴더가 있습니다.

응용 프로그램을 단일 어셈블리 로 배포 (제자리에 복사)하면서 여러 문화 별 리소스 집합을 포함 할 수있는 기능을 유지하고 싶습니다 .

ILMerge (또는 ILRepack )를 사용 하여 위성 어셈블리를 주 실행 가능 어셈블리에 병합 할 수 있지만 표준 .NET ResourceManager 대체 메커니즘은 주 어셈블리로 컴파일 된 문화 별 리소스를 찾지 못합니다.

흥미롭게도 병합 된 (실행 가능한) 어셈블리를 가져 와서 문화 별 하위 폴더에 복사하면 모든 것이 작동합니다! 마찬가지로 Reflector (또는 ILSpy )를 사용할 때 병합 된 어셈블리에서 주요 및 문화 별 리소스를 볼 수 있습니다 . 그러나 주 어셈블리를 문화 별 하위 폴더로 복사하면 병합의 목적이 어차피 해집니다. 단일 어셈블리의 단일 복사본 만 있으면됩니다.

GAC 및 문화권 이름이 지정된 하위 폴더가 아닌 동일한 어셈블리에서 문화권 별 리소스를 찾기 위해 ResourceManager 대체 메커니즘을 가로 채거나 영향을 미칠 수있는 방법이 있는지 궁금 합니다 . 다음 문서에 설명 된 대체 메커니즘이 있지만 수정 방법에 대한 단서는 없습니다 . ResourceManager의 BCL Team Blog Article .

누구든지 아이디어가 있습니까? 이것은 온라인에서 비교적 빈번한 질문 인 것처럼 보이지만 (예를 들어 Stack Overflow에 대한 또 다른 질문 : " ILMerge 및 지역화 된 리소스 어셈블리 ") 어디에서도 신뢰할 수있는 답변을 찾지 못했습니다.


업데이트 1 : 기본 솔루션

아래의 casperOne의 권장 사항에 따라 마침내이 작업을 수행 할 수있었습니다.

casperOne이 유일한 답을 제공했기 때문에 여기에 솔루션 코드를 넣습니다. 내 자신을 추가하고 싶지 않습니다.

"InternalGetResourceSet"메서드에 구현 된 프레임 워크 리소스 찾기 대체 메커니즘에서 배짱을 빼고 동일한 어셈블리 검색을 사용 된 첫 번째 메커니즘으로 만들어 작동하도록 할 수있었습니다 . 현재 어셈블리에서 리소스를 찾을 수없는 경우 기본 검색 메커니즘을 시작하는 기본 메서드를 호출합니다 (아래 @Wouter의 설명 덕분에).

이를 위해 "ComponentResourceManager"클래스를 파생하고 하나의 메서드 만 덮어 쓰고 개인 프레임 워크 메서드를 다시 구현했습니다.

class SingleAssemblyComponentResourceManager : 
    System.ComponentModel.ComponentResourceManager
{
    private Type _contextTypeInfo;
    private CultureInfo _neutralResourcesCulture;

    public SingleAssemblyComponentResourceManager(Type t)
        : base(t)
    {
        _contextTypeInfo = t;
    }

    protected override ResourceSet InternalGetResourceSet(CultureInfo culture, 
        bool createIfNotExists, bool tryParents)
    {
        ResourceSet rs = (ResourceSet)this.ResourceSets[culture];
        if (rs == null)
        {
            Stream store = null;
            string resourceFileName = null;

            //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done);
            if (this._neutralResourcesCulture == null)
            {
                this._neutralResourcesCulture = 
                    GetNeutralResourcesLanguage(this.MainAssembly);
            }

            // if we're asking for the default language, then ask for the
            // invariant (non-specific) resources.
            if (_neutralResourcesCulture.Equals(culture))
                culture = CultureInfo.InvariantCulture;
            resourceFileName = GetResourceFileName(culture);

            store = this.MainAssembly.GetManifestResourceStream(
                this._contextTypeInfo, resourceFileName);

            //If we found the appropriate resources in the local assembly
            if (store != null)
            {
                rs = new ResourceSet(store);
                //save for later.
                AddResourceSet(this.ResourceSets, culture, ref rs);
            }
            else
            {
                rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents);
            }
        }
        return rs;
    }

    //private method in framework, had to be re-specified here.
    private static void AddResourceSet(Hashtable localResourceSets, 
        CultureInfo culture, ref ResourceSet rs)
    {
        lock (localResourceSets)
        {
            ResourceSet objA = (ResourceSet)localResourceSets[culture];
            if (objA != null)
            {
                if (!object.Equals(objA, rs))
                {
                    rs.Dispose();
                    rs = objA;
                }
            }
            else
            {
                localResourceSets.Add(culture, rs);
            }
        }
    }
}

이 클래스를 실제로 사용하려면 Visual Studio에서 만든 "XXX.Designer.cs"파일에서 System.ComponentModel.ComponentResourceManager를 교체해야합니다. 디자인 된 양식을 변경할 때마다이 작업을 수행해야합니다. Visual Studio는이를 대체합니다. 자동으로 코드. (이 문제는 " MyResourceManager를 사용하도록 Windows Forms 디자이너 사용자 지정 "에서 설명했습니다 . 더 우아한 솔루션을 찾지 못했습니다. 빌드 전 단계에서 fart.exe사용 하여 자동으로 교체합니다.)


업데이트 2 : 또 다른 실용적인 고려 사항-2 개 이상의 언어

위의 솔루션을보고했을 때 저는 실제로 두 가지 언어 만 지원했고 ILMerge는 위성 어셈블리를 최종 병합 어셈블리로 병합하는 작업을 잘 수행했습니다.

최근에 저는 여러 개의 보조 언어가 있고 따라서 여러 개의 위성 어셈블리 가있는 유사한 프로젝트에서 작업을 시작했고 ILMerge는 매우 이상한 일을하고있었습니다. 내가 요청한 여러 위성 어셈블리를 병합하는 대신 여러 번에 걸쳐 첫 번째 위성 어셈블리를 병합했습니다. !

예 : 명령 줄 :

"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1InputProg.exe %1es\InputProg.resources.dll %1fr\InputProg.resources.dll

해당 명령 줄을 사용하여 병합 된 어셈블리에서 다음 리소스 집합을 얻었습니다 (ILSpy 디 컴파일러로 관찰).

InputProg.resources
InputProg.es.resources
InputProg.es.resources <-- Duplicated!

몇 번 놀아 본 후 , 단일 명령 줄 호출에서 동일한 이름가진 여러 파일을 만날 때 이것이 ILMerge의 버그 일 뿐이라는 것을 깨달았습니다 . 해결책은 단순히 각 위성 어셈블리를 다른 명령 줄 호출로 병합하는 것입니다.

"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll
"c:\Program Files\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll

이렇게하면 최종 어셈블리의 결과 리소스가 정확합니다.

InputProg.resources
InputProg.es.resources
InputProg.fr.resources

마지막으로 이것이 명확하게 도움이되는 경우 완전한 빌드 후 배치 파일입니다.

"%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1TempProg.exe %1InputProg.exe %1es\InputProg.resources.dll 
IF %ERRORLEVEL% NEQ 0 GOTO END

"%ProgramFiles%\Microsoft\ILMerge\ILMerge.exe" /t:exe /out:%1SomeFinalProg.exe %1TempProg.exe %1fr\InputProg.resources.dll 
IF %ERRORLEVEL% NEQ 0 GOTO END

del %1InputProg.exe 
del %1InputProg.pdb 
del %1TempProg.exe 
del %1TempProg.pdb 
del %1es\*.* /Q 
del %1fr\*.* /Q 
:END

업데이트 3 : ILRepack

또 다른 빠른 메모-ILMerge로 저를 괴롭혔던 것 중 하나는 Visual Studio와 함께 기본적으로 설치되지 않는 추가 독점 Microsoft 도구이므로 타사가 시작하기가 조금 더 어려워지는 추가 종속성이 있다는 것입니다. 내 오픈 소스 프로젝트와 함께.

나는 최근 에 오픈 소스 (Apache 2.0)에 상응하는 ILRepack을 발견 했습니다. 지금까지 나에게도 잘 작동하며 (드롭 인 교체) 프로젝트 소스와 함께 자유롭게 배포 할 수 있습니다.


나는 이것이 누군가를 돕기를 바랍니다!


이 작업을 볼 수있는 유일한 방법 ResourceManagerInternalGetResourceSetGetResourceFileName메서드 에서 파생 된 클래스를 만든 다음 재정의하는 것 입니다. 여기에서 CultureInfo인스턴스가 주어지면 리소스를 얻는 위치를 재정의 할 수 있어야 합니다.


다른 접근 방식 :

1) resource.DLL을 프로젝트에 포함 된 리소스로 추가합니다.

2) AppDomain.CurrentDomain.ResourceResolve에 대한 이벤트 처리기를 추가합니다. 이 핸들러는 리소스를 찾을 수 없을 때 실행됩니다.

  internal static System.Reflection.Assembly CurrentDomain_ResourceResolve(object sender, ResolveEventArgs args)
        {
            try
            {
                if (args.Name.StartsWith("your.resource.namespace"))
                {
                    return LoadResourcesAssyFromResource(System.Threading.Thread.CurrentThread.CurrentUICulture, "name of your the resource that contains dll");
                }
                return null;
            }
            catch (Exception ex)
            {
                return null;
            }
        }

3) 이제 다음과 같이 LoadResourceAssyFromResource를 구현해야합니다.

private Assembly LoadResourceAssyFromResource( Culture culture, ResourceName resName)
        {
                    //var x = Assembly.GetExecutingAssembly().GetManifestResourceNames();

                    using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resName))
                    {
                        if (stream == null)
                        {
                            //throw new Exception("Could not find resource: " + resourceName);
                            return null;
                        }

                        Byte[] assemblyData = new Byte[stream.Length];

                        stream.Read(assemblyData, 0, assemblyData.Length);

                        var ass = Assembly.Load(assemblyData);

                        return ass;
        }

}


문제의 일부에 대한 제안이 있습니다. 특히, ComponentResourceManager를 SingleAssemblyComponentResourceManager로 대체하기 위해 .Designer.cs 파일을 업데이트하는 단계에 대한 솔루션입니다.

  1. InitializeComponent () 메서드를 .Designer.cs에서 구현 파일 (#region 포함)로 이동합니다. Visual Studio는 내가 말할 수있는 한 문제없이 해당 섹션을 계속 자동 생성합니다.

  2. ComponentResourceManager가 SingleAssemblyComponentResourceManager에 별칭이 지정되도록 구현 파일의 맨 위에 C # 별칭을 사용합니다.

불행히도 나는 이것을 완전히 테스트하지 못했습니다. 우리는 문제에 대한 다른 해결책을 찾았고 계속 진행했습니다. 그래도 도움이 되었기를 바랍니다.


그냥 생각.

단계를 수행하고 SingleAssemblyComponentResourceManager

그렇다면 왜 당신은 인공위성 어셈블리를 불충분 한 어셈블리에 포함시키는 데 어려움을 겪습니까?

ResourceName.es.resx프로젝트의 다른 리소스에 이진 파일로 자체를 추가 할 수 있습니다 .

코드를 다시 작성할 수있는 것보다

       store = this.MainAssembly.GetManifestResourceStream(
            this._contextTypeInfo, resourceFileName);

//If we found the appropriate resources in the local assembly
if (store != null)
{
    rs = new ResourceSet(store);

이 코드로 (테스트되지 않았지만 작동해야 함)

// we expect the "main" resource file to have a binary resource
// with name of the local (linked at compile time of course)
// which points to the localized resource
var content = Properties.Resources.ResourceManager.GetObject("es");
if (content != null)
{
    using (var stream = new MemoryStream(content))
    using (var reader = new ResourceReader(stream))
    {
        rs = new ResourceSet(reader);
    }
}

이것은 ilmerge 프로세스에 위성 어셈블리를 포함하려는 노력을 쓸모 없게 만들 것입니다.


댓글이 충분한 공간을 제공하지 않았기 때문에 답변으로 게시 됨 :

OP 솔루션으로 중립 문화 ( en대신 en-US)에 대한 리소스를 찾을 수 없습니다 . 그래서 나는 InternalGetResourceSet나를 위해 일한 중립 문화에 대한 검색으로 확장 했습니다. 이를 통해 이제 지역을 정의하지 않는 리소스를 찾을 수도 있습니다. 이것은 실제로 리소스 파일을 ILMerging하지 않을 때 일반 리소스 포맷터가 표시하는 것과 동일한 동작입니다.

//Try looking for the neutral culture if the specific culture was not found
if (store == null && !culture.IsNeutralCulture)
{
    resourceFileName = GetResourceFileName(culture.Parent);

    store = this.MainAssembly.GetManifestResourceStream(
                    this._contextTypeInfo, resourceFileName);
}

이로 인해 다음 코드가 생성됩니다. SingleAssemblyComponentResourceManager

class SingleAssemblyComponentResourceManager : 
    System.ComponentModel.ComponentResourceManager
{
    private Type _contextTypeInfo;
    private CultureInfo _neutralResourcesCulture;

    public SingleAssemblyComponentResourceManager(Type t)
        : base(t)
    {
        _contextTypeInfo = t;
    }

    protected override ResourceSet InternalGetResourceSet(CultureInfo culture, 
        bool createIfNotExists, bool tryParents)
    {
        ResourceSet rs = (ResourceSet)this.ResourceSets[culture];
        if (rs == null)
        {
            Stream store = null;
            string resourceFileName = null;

            //lazy-load default language (without caring about duplicate assignment in race conditions, no harm done);
            if (this._neutralResourcesCulture == null)
            {
                this._neutralResourcesCulture = 
                    GetNeutralResourcesLanguage(this.MainAssembly);
            }

            // if we're asking for the default language, then ask for the
            // invariant (non-specific) resources.
            if (_neutralResourcesCulture.Equals(culture))
                culture = CultureInfo.InvariantCulture;
            resourceFileName = GetResourceFileName(culture);

            store = this.MainAssembly.GetManifestResourceStream(
                this._contextTypeInfo, resourceFileName);

            //Try looking for the neutral culture if the specific culture was not found
            if (store == null && !culture.IsNeutralCulture)
            {
                resourceFileName = GetResourceFileName(culture.Parent);

                store = this.MainAssembly.GetManifestResourceStream(
                    this._contextTypeInfo, resourceFileName);
            }                

            //If we found the appropriate resources in the local assembly
            if (store != null)
            {
                rs = new ResourceSet(store);
                //save for later.
                AddResourceSet(this.ResourceSets, culture, ref rs);
            }
            else
            {
                rs = base.InternalGetResourceSet(culture, createIfNotExists, tryParents);
            }
        }
        return rs;
    }

    //private method in framework, had to be re-specified here.
    private static void AddResourceSet(Hashtable localResourceSets, 
        CultureInfo culture, ref ResourceSet rs)
    {
        lock (localResourceSets)
        {
            ResourceSet objA = (ResourceSet)localResourceSets[culture];
            if (objA != null)
            {
                if (!object.Equals(objA, rs))
                {
                    rs.Dispose();
                    rs = objA;
                }
            }
            else
            {
                localResourceSets.Add(culture, rs);
            }
        }
    }
}

참조 URL : https://stackoverflow.com/questions/1952638/single-assembly-multi-language-windows-forms-deployment-ilmerge-and-satellite-a

반응형