62 votes

Déploiement de Windows Forms multi-langues à assemblage unique (ILMerge et assemblages satellites / localisation) - possible ?

J'ai une simple application Windows Forms (C#, .NET 2.0), construite avec Visual Studio 2008.

J'aimerais prendre en charge plusieurs langues d'interface utilisateur et, grâce à la propriété "Localizable" du formulaire et aux fichiers .resx spécifiques à une culture, l'aspect localisation fonctionne facilement et de manière transparente. Visual Studio compile automatiquement les fichiers resx spécifiques à la culture dans des assemblages satellites. Ainsi, dans le dossier de mon application compilée, il existe des sous-dossiers spécifiques à la culture contenant ces assemblages satellites.

Je voudrais que l'application soit déployée (copiée en place) en tant que assemblée unique tout en conservant la possibilité de contenir plusieurs ensembles de ressources spécifiques à une culture.

Utilisation de ILMerge (ou ILRepack ), je peux fusionner les assemblages satellites dans l'assemblage exécutable principal, mais les mécanismes de repli standard du ResourceManager .NET ne trouvent pas les ressources spécifiques à la culture qui ont été compilées dans l'assemblage principal.

Il est intéressant de noter que si je prends mon assemblage fusionné (exécutable) et que je place des copies de celui-ci dans les sous-dossiers spécifiques à la culture, tout fonctionne ! De même, je peux voir les ressources principales et celles spécifiques à la culture dans l'assemblage fusionné lorsque j'utilise la commande Réflecteur (ou ILSpy ). Mais copier l'assemblage principal dans des sous-dossiers spécifiques à chaque culture va à l'encontre de l'objectif de la fusion - j'ai vraiment besoin d'une seule copie de l'assemblage unique...

Je me demande s'il existe un moyen de détourner ou d'influencer les mécanismes de repli du ResourceManager pour qu'ils recherchent les ressources spécifiques à une culture dans le même assemblage plutôt que dans le GAC et dans des sous-dossiers portant un nom de culture. . Je vois le mécanisme de repli décrit dans les articles suivants, mais je n'ai aucune idée de la manière dont il pourrait être modifié : Article du blog de l'équipe BCL sur ResourceManager .

Quelqu'un a-t-il une idée ? Cela semble être une question relativement fréquente en ligne (par exemple, une autre question ici sur Stack Overflow : " ILMerge et assemblages de ressources localisées "), mais je n'ai trouvé aucune réponse faisant autorité nulle part.


UPDATE 1 : Solution de base

Suivant La recommandation de casperOne ci-dessous j'ai finalement réussi à le faire fonctionner.

Je mets le code de la solution ici dans la question parce que casperOne a fourni la seule réponse, je ne veux pas ajouter la mienne.

J'ai réussi à le faire fonctionner en retirant l'essentiel des mécanismes de repli de la recherche de ressources du Framework mis en œuvre dans la méthode "InternalGetResourceSet" et en faisant en sorte que la recherche dans le même assemblage soit effectuée par la méthode "InternalGetResourceSet". premièrement utilisé. Si la ressource n'est pas trouvée dans l'assemblage actuel, alors nous appelons la méthode base pour lancer les mécanismes de recherche par défaut (merci au commentaire de @Wouter ci-dessous).

Pour ce faire, j'ai dérivé la classe "ComponentResourceManager", et surrodé une seule méthode (et réimplémenté une méthode privée du framework) :

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

Pour utiliser réellement cette classe, vous devez remplacer le System.ComponentModel.ComponentResourceManager dans les fichiers "XXX.Designer.cs" créés par Visual Studio - et vous devrez le faire chaque fois que vous modifiez le formulaire conçu - Visual Studio remplace ce code automatiquement. (Ce problème a été abordé dans " Personnaliser Windows Forms Designer pour utiliser MyResourceManager ", je n'ai pas trouvé de solution plus élégante - j'utilise fart.exe dans une étape de pré-construction pour remplacer automatiquement).


MISE À JOUR 2 : une autre considération pratique - plus de 2 langues

Au moment où j'ai signalé la solution ci-dessus, je ne supportais en fait que deux langues, et ILMerge faisait un bon travail de fusion de mon assemblage satellite dans l'assemblage fusionné final.

Récemment, j'ai commencé à travailler sur un projet similaire où il y a multiple des langues secondaires, et donc des assemblages satellites multiples, et ILMerge faisait quelque chose de très étrange : Au lieu de fusionner les multiples assemblages satellites que j'avais demandés, il fusionnait plusieurs fois le premier assemblage satellite !

eg ligne de commande :

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

Avec cette ligne de commande, j'ai obtenu les ensembles de ressources suivants dans l'assemblage fusionné (observé avec le décompilateur ILSpy) :

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

Après avoir joué un peu, j'ai fini par réaliser que c'est juste un bug dans ILMerge quand il rencontre plusieurs fichiers avec le même nom en un seul appel de ligne de commande. La solution consiste simplement à fusionner chaque assemblage de satellite dans un appel de ligne de commande différent :

"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

Lorsque je fais cela, les ressources résultantes dans l'assemblage final sont correctes :

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

Enfin, au cas où cela aiderait à clarifier les choses, voici un fichier batch post-construction complet :

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

Une autre note rapide - Une des choses qui m'a dérangé avec ILMerge est qu'il s'agit d'un outil propriétaire supplémentaire de Microsoft, qui n'est pas installé par défaut avec Visual Studio, et donc une dépendance supplémentaire qui rend un peu plus difficile pour un tiers de démarrer avec mes projets open-source.

J'ai récemment découvert ILRepack un équivalent open-source (Apache 2.0) qui, jusqu'à présent, fonctionne aussi bien pour moi (remplacement immédiat), et peut être distribué gratuitement avec les sources de votre projet.


J'espère que cela aidera quelqu'un d'autre !

1 votes

Après quelques recherches supplémentaires (relativement obsessionnelles), j'ai trouvé quelques extraits prometteurs : neowin.net/forum/lofiversion/index.php/t625641.html social.msdn.microsoft.com/Forums/fr/vsx/thread/ Il semble que la création d'un ResourceManager personnalisé soit la voie à suivre - je vais décompiler le ResourceManager par défaut (Reflector à la rescousse !) pour voir si je peux mieux comprendre ce qu'il fait / comment il fonctionne.

0 votes

Bonjour. J'ai besoin de faire exactement la même chose. J'ai ajouté cette nouvelle classe et l'ai remplacée dans un fichier de conception, mais je reçois une NullReferenceException à la ligne : this.outputPath.Properties.AutoHeight = ((bool)(resources.GetObject("outputPath.Properties.AutoHeight")) ; (resources est un SingleAssemblyComponentResourceManager) outputPath - est un contrôle DevExpress. Même après avoir fusionné tous les assemblages DevExpress.

0 votes

Savez-vous ce qui est réellement Null ? Est-ce l'objet "ressources" qui n'est pas réellement initialisé ? Je ne suis pas sûr que ce soit le meilleur forum pour vous aider à travailler sur votre code, mais n'hésitez pas à m'envoyer des détails à stuff at klerks dot biz.

27voto

casperOne Points 49736

La seule façon de faire fonctionner ce système est de créer une classe qui dérive de ResourceManager puis de remplacer le InternalGetResourceSet y GetResourceFileName méthodes. À partir de là, vous devriez être en mesure de modifier l'endroit où les ressources sont obtenues, compte tenu d'un CultureInfo instance.

0 votes

Merci, j'ai finalement réussi à le faire fonctionner, j'ai ajouté mon code ci-dessus car le vôtre était techniquement la réponse. Notez que la seule méthode qui avait réellement besoin d'être surchargée était "InternalGetResourceSet". J'aurais aimé ne la modifier que si nécessaire, mais le code existant faisait un usage intensif d'autres codes internes et/ou privés du framewoprk, il était plus facile de tout arracher et de n'implémenter que le chargement local de l'assemblage.

4voto

jm. Points 7092

Une approche différente :

1) ajoutez vos resource.DLLs comme ressources incorporées dans votre projet.

2) ajouter un gestionnaire d'événement pour AppDomain.CurrentDomain.ResourceResolve Ce gestionnaire se déclenche lorsqu'une ressource ne peut être trouvée.

      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) Maintenant, vous devez implémenter LoadResourceAssyFromResource de la façon suivante

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

1voto

Marwie Points 1295

Posté en tant que réponse puisque les commentaires n'offraient pas assez d'espace :

Je n'ai pas trouvé de ressources pour les cultures neutres ( en au lieu de en-US ) avec la solution du PO. J'ai donc étendu InternalGetResourceSet avec une recherche de cultures neutres qui a fait l'affaire pour moi. Avec cela, vous pouvez maintenant aussi localiser les ressources qui ne définissent pas la région. Il s'agit en fait du même comportement que celui du formateur de ressources normal lorsqu'il ne fusionne pas les fichiers de ressources.

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

Il en résulte le code suivant pour l'élément 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);
            }
        }
    }
}

0voto

Josh Buedel Points 891

J'ai une suggestion pour une partie de votre problème. Plus précisément, une solution à l'étape de mise à jour des fichiers .Designer.cs pour remplacer ComponentResourceManager par SingleAssemblyComponentResourceManager.

  1. Déplacez la méthode InitializeComponent() hors de .Designer.cs et dans le fichier d'implémentation (incluez la #region). Visual Studio continuera à générer automatiquement cette section, sans aucun problème, pour autant que je sache.

  2. Utilisez un alias C# en haut du fichier d'implémentation afin que ComponentResourceManager soit aliasé en SingleAssemblyComponentResourceManager.

Malheureusement, je n'ai pas eu l'occasion de le tester complètement. Nous avons trouvé une autre solution à notre problème et sommes passés à autre chose. J'espère cependant que cela vous aidera.

1 votes

Merci pour cette suggestion, elle est intéressante (je ne savais pas que l'on pouvait déplacer la méthode InitializeComponent et faire en sorte que le designer continue à fonctionner) ! Malheureusement, chaque fois que le concepteur remplace la référence "ComponentResourceManager", il utilise le nom de type entièrement qualifié "System.ComponentModel.ComponentResourceManager", donc l'alias ne semble pas aider. Merci encore pour l'astuce !

0voto

SchlaWiener Points 9682

Juste une idée.

Vous avez fait l'étape et créé votre SingleAssemblyComponentResourceManager

Alors pourquoi prenez-vous la peine d'inclure vos assemblages de satellites dans l'Assemblée ilmergée ?

Vous pourriez ajouter le ResourceName.es.resx en tant que fichier binaire vers une autre ressource de votre projet.

Vous pourriez alors réécrire votre code

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

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

avec ce code (non testé mais qui devrait fonctionner)

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

Cela devrait rendre obsolète l'effort d'inclure les assemblées sattelites dans le processus d'ilmerge.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X