Wdrożenie systemu Windows Forms w jednym złożeniu (ILMerge i satellite assemblies / localization) - możliwe?

Mam prostą aplikację Windows Forms (C#,. Net 2.0), zbudowaną z Visual Studio 2008.

Chciałbym obsługiwać wiele języków interfejsu użytkownika i używać właściwości " Localizable "formularza i specyficznych dla kultury.pliki resx, aspekt lokalizacji działa płynnie i łatwo. Visual Studio automatycznie kompiluje specyficzne dla kultury pliki resx do zestawów satelitarnych, więc w folderze mojej skompilowanej aplikacji znajdują się specyficzne dla kultury podfoldery zawierające te satelitarne / align = "left" /

Chciałbym, aby aplikacja została wdrożona (skopiowana na miejsce) jako pojedyncze Zgromadzenie, a jednocześnie zachować możliwość przechowywania wielu zestawów zasobów specyficznych dla kultury.

Używając ILMerge (lub ILRepack ), mogę scalić zestawy satelitarne do głównego zestawu wykonywalnego, ale standardowe mechanizmy awaryjne. NET ResourceManager nie znajdują specyficznych dla kultury zasobów, które zostały skompilowane do głównego zestawu wykonywalnego. montaż.

Co ciekawe, jeśli wezmę mój scalony (wykonywalny) assembly i umieszczę jego kopie w specyficznych dla kultury podfolderach, to wszystko działa! Podobnie, mogę zobaczyć główne i specyficzne dla kultury zasoby w scalonym asemby, gdy używam Reflector (lub ILSpy). Ale kopiowanie głównego zestawu do specyficznych dla kultury podfolderów i tak niszczy cel scalania-naprawdę potrzebuję tylko jednej kopii pojedynczego zestawu...

I ' m zastanawiając się , czy istnieje jakiś sposób na przejęcie lub wpływ mechanizmów rezerwowych ResourceManager, aby szukać zasobów specyficznych dla kultury w tym samym zbiorze, a nie w podfolderach GAC i nazwanych kulturą. Widzę mechanizm awaryjny opisany w poniższych artykułach, ale nie mam pojęcia, jak go zmodyfikować: BCL Team Blog artykuł na temat ResourceManager .

Czy ktoś ma jakiś pomysł? Wydaje się, że jest to stosunkowo częste pytanie w Internecie (na przykład, kolejne pytanie na Stack Overflow: "ILMerge and localized resource assemblies "), ale nigdzie nie znalazłem żadnej autorytatywnej odpowiedzi.
UPDATE 1: Podstawowe rozwiązanie]}

Zgodnie z zaleceniem casperOne poniżej , W końcu udało mi się to zrobić.

Umieszczam kod rozwiązania tutaj w pytaniu, ponieważ kacperone podał jedyną odpowiedź, nie chcę dodawać własnej.

Udało mi się go uruchomić, wyciągając flaki z mechanizmów Framework resource-finding fallback zaimplementowanych w metodzie "InternalGetResourceSet" i dokonujących naszego wyszukiwania w tym samym zestawie zastosowany jest pierwszy mechanizm. Jeśli zasób nie znajduje się w bieżącym zestawieniu, to wywołujemy metodę bazową, aby zainicjować domyślne mechanizmy wyszukiwania(dzięki komentarzowi @ Wouter poniżej).

Aby to zrobić, wyprowadziłem klasę "ComponentResourceManager" i nadpisałem tylko jedną metodę (i ponownie zaimplementowałem prywatny framework "metoda"): {]}

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);
            }
        }
    }
}

Aby użyć tej klasy, musisz wymienić System.ComponentModel.ComponentResourceManager w "xxx. Designer.pliki cs " stworzone przez Visual Studio - i będziesz musiał to robić za każdym razem, gdy zmienisz projektowany formularz-Visual Studio automatycznie zastąpi ten kod. (Problem był omawiany w " Customize Windows Forms Designer to use MyResourceManager", nie znalazłem bardziej eleganckiego rozwiązania - używam fart.exe w kroku pre-build do auto-replace.)


UPDATE 2: kolejna praktyczna Uwaga - więcej niż 2 języki

W czasie, gdy zgłosiłem powyższe rozwiązanie, w rzeczywistości obsługiwałem tylko dwa języki, a ILMerge wykonywał świetną robotę scalania mojego zestawu satelitarnego w końcowy scalony zestaw.

Ostatnio zacząłem pracować nad podobnym projektem, w którym jest wiele języków drugorzędnych, a więc wiele zestawów satelitarnych, a ILMerge robił coś bardzo dziwne: zamiast łączyć wiele zestawów satelitów, o które prosiłem, łączyło się to wielokrotnie z pierwszym zestawem satelitów!

Np command-line:

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

Z tym wierszem poleceń, otrzymywałem następujące zestawy zasobów w scalonym asemblerze (zaobserwowanym przy dekompilatorze ILSpy):

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

Po kilku zabawach, zdałem sobie sprawę, że to tylko błąd w ILMerge, gdy napotka wiele plików o tej samej nazwie W Jednym wywołanie z linii poleceń. Rozwiązaniem jest po prostu połączenie każdego zestawu satelitarnego w innym wywołaniu wiersza poleceń:

"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

Kiedy to zrobię, wynikowe zasoby w końcowym zbiorze są poprawne:

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

Więc na koniec, jeśli to pomoże wyjaśnić, Oto kompletny plik wsadowy po zbudowaniu:

"%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

UPDATE 3: ILRepack

Kolejna krótka notka - jedną z rzeczy, która mnie niepokoiła ILMerge, było to, że jest to dodatkowe, własnościowe narzędzie Microsoftu, nie zainstalowane domyślnie z Visual Studio, a tym samym dodatkową zależnością, która sprawia, że trochę trudniej osobom trzecim rozpocząć pracę z moimi projektami open-source.

Niedawno odkryłem ILRepack , odpowiednik open-source (Apache 2.0), który do tej pory działa równie dobrze dla mnie( zamiennik drop-in) i może być swobodnie dystrybuowany ze źródłami twojego projektu.


Mam nadzieję, że to komuś pomoże!
Author: Community, 2009-12-23

5 answers

Jedyny sposób, w jaki widzę to działanie, to stworzenie klasy, która wywodzi się z ResourceManager / align = "left" / InternalGetResourceSet oraz GetResourceFileName metody. Stamtąd powinieneś być w stanie nadpisać, gdzie zasoby są pozyskiwane, biorąc pod uwagę CultureInfo przykład.

 23
Author: casperOne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-12 15:09:50

Inne podejście:

1) Dodaj swój zasób.Biblioteki dll jako osadzone zasoby w projekcie.

2) Dodaj obsługę zdarzeń dla AppDomain.CurrentDomain.ResourceResolve. Ta funkcja obsługi uruchomi się, gdy zasobu nie można znaleźć.

  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) Teraz musisz zaimplementować LoadResourceAssyFromResource coś w stylu

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;
        }

}

 1
Author: jm.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-17 22:31:58

Mam propozycję dla części Twojego problemu. W szczególności rozwiązanie na etapie aktualizacji .Projektant.pliki cs do zastąpienia ComponentResourceManager przez SingleAssemblyComponentResourceManager.

  1. Przenieś metodę InitializeComponent () z .Projektant.cs i do pliku implementacji(Dołącz region#). Visual Studio będzie nadal automatycznie generować tę sekcję, bez żadnych problemów, o ile mogę powiedzieć.

  2. Użyj aliasu C# na górze plik implementacji tak, że ComponentResourceManager jest aliasowany do SingleAssemblyComponentResourceManager.

Niestety nie udało mi się tego w pełni przetestować. Znaleźliśmy inne rozwiązanie naszego problemu i ruszyliśmy dalej. Mam nadzieję, że to ci pomoże.

 0
Author: Josh Buedel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-03-18 15:58:57

Tak sobie pomyślałem.

Zrobiłeś krok i stworzyłeś swój SingleAssemblyComponentResourceManager

Więc dlaczego bierzesz na siebie ból, aby włączyć swoje zespoły satelitarne do ilmerged Assembly?

Możesz dodać ResourceName.es.resx jako plik binarny do innego zasobu w Twoim projekcie.

Niż mógłbyś przepisać swój kod

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

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

Z tym kodem (nie testowany, ale powinien działać)

// 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);
    }
}

Powinno to uczynić wysiłek, aby włączyć Asembly sattelite w proces ilmerge przestarzałe.

 0
Author: Jürgen Steinblock,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-02-06 15:35:19

Posted as answer since comments didn ' t provide enough space:

Nie mogłem znaleźć zasobów dla neutralnych kultur (en zamiast en-US) za pomocą rozwiązania OPs. Więc rozszerzyłem InternalGetResourceSet o Wyszukiwanie neutralnych kultur, które wykonały zadanie dla mnie. Dzięki temu możesz teraz również zlokalizować zasoby, które nie definiują regionu. To jest rzeczywiście to samo zachowanie, które normalne resourceformatter pokaże, gdy nie ILMerging plików zasobów.

//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);
}

To powoduje, że następujący kod dla 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);
            }
        }
    }
}
 0
Author: Marwie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-04 13:38:42