Pourquoi faire simple quand on peu faire compliqué

Parce que la vie d'un programmeur n'est jamais simple et qu'il faut toujours s'arracher les cheveux sur des problèmes incongrus :)

Créer un installateur qui choisit automatiquement les bons fichiers pour les plateformes x64 et x86


Le problème : Une application (en l'occurrence, il s'agit de Zappiti, le média manager pour platine multimédia Dune HDi) utilise une DLL s'appelant MediaInfo.dll et qui est fournie en version x86 et x64. Zappiti est compilé en mode AnyCPU et peux donc fonctionner avec l'une ou l'autre du moment que la DLL retenue corresponde à la plateforme du système d'exploitation courant.

L'objectif : Créer un installateur avec Visual Studio Installer qui détecte si l'application s'installe sur une architecture 32bit ou 64bit et qui installe la DLL correspondante.

Commençons par ajouter les DLL : Première étape, ajouter les deux versions de la DLL à notre installateur. Il n'est pas nécessaire de spécifier de Condition. Ma première approche fût justement de le faire en spécifiant TargetPlatform=x86 ou TargetPlatform=x64. Malheureusement, la valeur de cette propriété est définie dans les paramètres de l'installateur. De ce fait, il aurait fallut créer deux installateurs, un pour chaque plateforme, alors que c'est justement ce que je cherche à éviter. Non, pour choisir le bon fichier dynamiquement en fonction du système d'exploitation, il faut faire autrement.

La solution consiste à créer des Custom Actions pour choisir le bon fichier : Pour cela, commencez par ajouter un fichier de type de Installer Class dans votre projet comme indiqué ici. Il n'est pas nécessaire de créer un nouveau projet DLL, vous pouvez l'ajouter directement à votre exécutable. Visual Studio crée automatiquement une classe qui hérite de Installer. Ensuite, il suffit de surcharger la fonction OnBeforeInstall() de cette classe pour executer le code désiré au bon moment.

Etrangement, lors de l'installation OnBeforeInstall s'exécute après la copie des fichier. Ce n'est pas très grave ici, au contraire. Nous allons laisser l'installateur copier les deux DLL, puis en supprimer une et renommer l'autre. Maintenant, il faut dire à l'installer d'appeler cette fonction en créant un nouveau Custom Action comme indiqué ci dessous, et en choisissant pour cible le "Primary output" de notre projet qui contient la classe que nous venons de créer.

Détecter la plateforme de l'OS : En premier lieu, j'ai testé la valeur retournée par IntPtr.Size. En effet, sur une plateforme x86, la valeur retournée serait 4 et sur une plateforme x64 on obtiendrait un 8. Hélas, cette méthode retourne toujours 4 car souvenez vous, le TargetPlatform de notre installateur est définit à x86 pour pouvoir se lancer de n'importe quel PC.

Pour détecter la plateforme, nous allons utiliser la méthode GetNativeSystemInfo() en l'important de kernel32.dll.

 
        private enum Platform
        {
            X86,
            X64,
            Unknown
        }
 
        internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0;
        internal const ushort PROCESSOR_ARCHITECTURE_IA64 = 6;
        internal const ushort PROCESSOR_ARCHITECTURE_AMD64 = 9;
        internal const ushort PROCESSOR_ARCHITECTURE_UNKNOWN = 0xFFFF;
 
        [StructLayout(LayoutKind.Sequential)]
        internal struct SYSTEM_INFO
        {
            public ushort wProcessorArchitecture;
            public ushort wReserved;
            public uint dwPageSize;
            public IntPtr lpMinimumApplicationAddress;
            public IntPtr lpMaximumApplicationAddress;
            public UIntPtr dwActiveProcessorMask;
            public uint dwNumberOfProcessors;
            public uint dwProcessorType;
            public uint dwAllocationGranularity;
            public ushort wProcessorLevel;
            public ushort wProcessorRevision;
        };
 
        [DllImport("kernel32.dll")]
        internal static extern void GetNativeSystemInfo(ref SYSTEM_INFO lpSystemInfo);
 
        private static Platform GetPlatform()
        {
            SYSTEM_INFO sysInfo = new SYSTEM_INFO();
            GetNativeSystemInfo(ref sysInfo);
 
            switch (sysInfo.wProcessorArchitecture)
            {
                case PROCESSOR_ARCHITECTURE_AMD64:
                    return Platform.X64;
 
                case PROCESSOR_ARCHITECTURE_INTEL:
                    return Platform.X86;
 
                default:
                    return Platform.Unknown;
            }
        }
 

Et voila, il reste juste à renommer la DLL correspondante à la plateforme choisie :

 
        protected override void OnBeforeInstall(IDictionary savedState)
        {
            base.OnBeforeInstall(savedState);
 
            string location = Path.GetDirectoryName(
                    System.Reflection.Assembly.GetAssembly(GetType()).Location);
 
            if (File.Exists(Path.Combine(location, "MediaInfo.dll")))
                File.Delete(Path.Combine(location, "MediaInfo.dll"));
 
 
            Platform p = GetPlatform();
            if (p == Platform.X86)
            {
                File.Move(Path.Combine(location, "MediaInfo-x86.dll"),
                          Path.Combine(location, "MediaInfo.dll"));
                File.Delete(Path.Combine(location, "MediaInfo-x64.dll"));
            }
            else if (p == Platform.X64)
            {
                File.Move(Path.Combine(location, "MediaInfo-x64.dll"),
                          Path.Combine(location, "MediaInfo.dll"));
                File.Delete(Path.Combine(location, "MediaInfo-x86.dll"));
            }
        }
 

Lorsqu'on désinstalle, il faut penser à supprimer "MediaInfo.dll" qui n'est pas connue de l'installer. On peut procéder comme précédemment en créant un nouveau Custom Action qui fera le ménage :

 
        protected override void OnBeforeUninstall(IDictionary savedState)
        {
            base.OnBeforeUninstall(savedState);
 
 
            string location = Path.GetDirectoryName(
                          System.Reflection.Assembly.GetAssembly(GetType()).Location);
 
            File.Delete(Path.Combine(location, "MediaInfo.dll"));           
        }
 

C'est presque finit : L'installateur répond parfaitement au besoin de sélectionner automatiquement la DLL correspondante à la plateforme du système d'exploitation courant. Néanmoins, on se retrouve avec deux fichiers : un .MSI et un .EXE. Le premier est notre installateur et le second est un bootstrapper qui sert à vérifier et à installer les dépendances nécessaires pour que notre installateur puisse lui même se lancer tout les cas. C'est donc une bonne chose de les avoir, mais cela fait tout de même un fichier de trop à mon goût.

Il existe des solutions payantes pour fusionner les deux fichiers en un seul exécutable. Moi j'ai trouvé plus simple (et moins cher) : utiliser WinRar et générer un package avec l'option SFX : on peut lui indiquer d'extraire les fichiers dans un dossier temporaire, de lancer setup.exe et même personnaliser l'icone de notre exécutable. Maintenant, on a plus qu'un seul exécutable qui installe tout ce qu'il faut automatiquement. Il ne reste plus qu'a le distribuer !



AddThis Social Bookmark Button
 

Merci Visual Studio pour tous tes mots doux


Quoi de plus agréable, au petit matin, de découvrir le plaisant petit mot que me laisse Visual Studio après 10 minutes de compilation ! J'apprécie tout particulièrement la clarté avec laquelle tu t'exprimes parfois, dans le soucis constant de me faire gagner du temps et de me rendre la vie plus facile...

Error 37 error LNK2019: unresolved external symbol "private: static void __cdecl CImport::GetChannelsForThisMaterial(class daeSmartRef,class std::list > &)" (?GetChannelsForThisMaterial@CImport@@CAXV?$daeSmartRef@VdomMaterial@@@@AAV?$list@HV?$allocator@H@std@@@std@@@Z) referenced in function "private: static void __cdecl CImport::ReadMaterial(class C3DScene *,class daeSmartRef,class stdext::hash_map,class std::allocator >,unsigned int,class stdext::hash_compare,class std::allocator >,struct std::less,class std::allocator > > >,class std::allocator,class std::allocator > const ,unsigned int> > > &,class stdext::hash_map,class std::allocator >,unsigned int,class stdext::hash_compare,class std::allocator >,struct std::less,class std::allocator > > >,class std::allocator,class std::allocator > const ,unsigned int> > > &)" (?ReadMaterial@CImport@@CAXPAVC3DScene@@V?$daeSmartRef@VdomBind_material@@@@AAV?$hash_map@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@IV?$hash_compare@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@U?$less@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@2@@stdext@@V?$allocator@U?$pair@$$CBV?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@I@std@@@2@@stdext@@2@Z) import.obj collada

Allez, on décrypte maintenant :-)



AddThis Social Bookmark Button
 

[WPF|Silverlight] Définir la source d'un binding


Il y a plusieurs façons de définir la source d'un binding. Le plus simple consiste à faire appel au contenu de la propriété DataContext. Cette propriété à une particularité intéressante : lorsque sa valeur est définie pour un contrôle, elle est aussi transmise par default à tous ses fils. Ainsi, en affectant un objet au DataContext d'une Window, tout les contrôles de votre Window seront associés par défaut à cet objet. Bien sûr, vous pouvez redéfinir le DataContext de chaque contrôle à n'importe quel moment.

  • Binding sur le DataContext :
     <ListBox ItemsSource="{Binding}" />
  • Binding sur un autre élément :
     <Slider Name="_mySlider"/>
     <TextBlock Text="{Binding ElementName=_mySlider,Path=Value}"/>
  • Binding sur soi-même :
     <TextBlock Name="_selfName"
                Text="{Binding Name, RelativeSource={RelativeSource Self}}" />
  • Binding vers un parent :
     <Canvas Name="_canvas">
         <TextBlock Text="{Binding Path=Name, 
                                   RelativeSource=
      {RelativeSource FindAncestor, AncestorType={x:Type Canvas}}}" />
     </Canvas>
  • Binding vers le template parent :
     <ControlTemplate x:Key="_template">
         <TextBlock Background="{TemplateBinding Background}"/>
     </ControlTemplate>
  • Binding vers le data item précédent :
     <TextBlock Text="{Binding RelativeSource={RelativeSource PreviousData}}" />
  • Binding d'une propriété statique :
     <TextBlock Text="{x:Static local:Window1.AStaticProperty}"/>
  • Binding d'une ressource statique :
     <TextBlock Background="{StaticResource _blackBrush}"/>
  • Binding d'une ressource dynamique :
     <TextBlock Background="{DynamicResource _blackBrush}"/>
  • Binding sur un enum :

Là, les choses sont un peu plus complexes. Il existe deux méthodes à ma connaissance. Une qui utilise un ObjectDataProvider et une autre qui utilise les Markup Extensions. La deuxième solution me semble beaucoup plus efficace :

using System;
using System.Windows.Markup;
 
namespace EnumValuesMarkupExtensions
{
    [MarkupExtensionReturnType(typeof(Array))]
    public class EnumValuesExtension : MarkupExtension
    {
        public EnumValuesExtension()
        {
        }
 
        public EnumValuesExtension(Type enumType)
        {
            this.EnumType = enumType;
        }
 
        [ConstructorArgument("enumType")]
        public Type EnumType { get; set; }
 
        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return Enum.GetValues(EnumType);
        }
    }
}

Et en Xaml, il ne reste plus qu'à définir la propriété ItemsSource d'un ComboBox de cette façon :

<Window x:Class="EnumValuesMarkupExtension.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:EnumValuesMarkupExtension"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="Window1" Height="300" Width="300">
    <Grid>
        <ComboBox ItemsSource="{my:EnumValues sys:DayOfWeek}" SelectedIndex="0" />
    </Grid>
</Window>

Voir aussi :



AddThis Social Bookmark Button
 

[WPF|Silverlight] Petite révision sur le Binding


Le Binding est certainement l'une des fonctionnalités de .Net que j'affectionne le plus parce qu'elle est élégante et peut faire gagner pas mal de temps lorsqu'on la maitrise bien. Pourquoi élégante ? Parce qu'elle permet de faire une séparation très nette entre le code et l'interface. C'est pour cette raison qu'il s'agit d'un des principes clef des frameworks tels que MVVM.

Comme rien ne vaut mieux qu'un exemple pour comprendre les choses, imaginons les classes suivantes :

public class Produit
{
  public string Nom { get; set; }
  public double Prix { get; set; }
}
 
public class Magasin
{
  private readonly List<Produit> _produits = new List<Produit>();
  public List<Produit> Produits
  { 
    get
    {
      return _produits;
    }
  }
}
 

Si on souhaite afficher les produits de notre magasin, il suffit de créer en Xaml :

 
<ListBox x:Name="MonMagasin" ItemsSource="{Binding Produits}">
  <ListBox.ItemTemplate>
    <StackPanel Orientation="Horizontal">
      <TextBlock Text="{Binding Nom}" Margin="0,0,5,0" />
      <TextBlock Text="{Binding Prix}" />
    </StackPanel >
  </ListBox.ItemTemplate>
</ListBox>
 

Il ne reste plus qu'a affecter notre magasin au DataContext de la listbox pour lier le contrôle à nos données et le tour est joué :

Magasin magasin = new Magasin();
magasin.Produits.Add(new Produit { Nom = "Tomates", Prix = 10});
magasin.Produits.Add(new Produit { Nom = "Pommes", Prix = 12});
magasin.Produits.Add(new Produit { Nom = "Orange", Prix = 8});
MonMagasin.DataContext = magasin;
 

On obtient une listbox qui affiche les produits de notre magasin. Mais à ce stade, que ce passe t'il si quelque part dans le code, on modifie le prix d'un des produits ? Rien du tout. L'interface ne reflète pas les changements de notre propriété. Nous allons voir dans un prochain article comment y remédier.

Voir aussi :



AddThis Social Bookmark Button
 

WCF et IIS avec les sites web qui ont des identités multiples


Je profite du dernier post de Cyril Durand pour finaliser un petit article que j'avais entamé sans jamais vraiment le finir. Il concerne le problème du binding des services WFC sur un IIS qui gère plusieurs identités pour le même site web :

 This collection already contains an address with scheme http. There can be at most one address per scheme in this collection.
Parameter name: item


où pour les francophones :

Cette collection contient déjà une adresse avec le schéma http. Une adresse tout au plus par schéma est possible dans cette collection.
Nom du paramètre : item

Voici ce qui se passe lorsque vous avez un site web configuré avec plusieurs identités ( ex: www.siteweb.com et siteweb.com ) lorsque vous tentez d'accéder au fichier Service.svc de votre service WCF.

Pourquoi ? WCF ne supporte pas les identités multiples sur le même site. Comment faire dès lors pour résoudre cette épineuse situation ?

De nombreux blogs proposent des solutions, qui si elles ne résolvent pas réellement le problème, ont le mérite d'aider à comprendre comment tout cela fonctionne :

Sur developpez.net, un premier échanges m'a permit d'identifier que le problème venait effectivement des identités multiples. Les serveurs mutualisés qui n'y font pas attention sont les premiers touchés.

Une première solution est donnée par Rob Reynolds : Il faut dériver la classe ServiceHostFactory comme suit :

 
using System.ServiceModel;
using System.ServiceModel.Activation;
 
public class CustomServiceFactory : ServiceHostFactory
{
 
 private int baseAddressIndex = 0;
 
 protected override ServiceHost CreateServiceHost(System.Type serviceType,
   System.Uri[] baseAddresses)
 {
    return new ServiceHost(serviceType, baseAddresses[baseAddressIndex]);
 }
}
 

Puis, dans le fichier *.svc :

 
<%@ ServiceHost Language="VB" Debug="true"
Service="Organization.Services.TempService"
Factory="Organization.Services.CustomServiceFactory" %>
 

Premier problème, comment connaitre l'indice de l'adresse qu'on souhaite utiliser ?
Deuxième problème, que se passe t'il si notre service WPF est instancié pour le nom de domaine www.siteweb.com une première fois (là ça marche) et qu'on tente d'y accéder ensuite via siteweb.com (là, ça marche plus).

L'idée n'était pourtant pas si mal et venait d'ici à l'origine.

Deux autres échanges ici et m'ont finalement menés à une solution qui semblait prometteuse. Mais voila, la solution du filtre permet en pratique de faire fonctionner WCF avec une seule identité. Or, je ne souhaite pas choisir en www.siteweb.com et siteweb.com, je veux que la solution marche dans tout les cas !

Je n'ai pas testé la solution de Cyril qui semble avoir réussit à gérer les différentes identités en se passant du fichier de configuration WFC. Pour des raisons de performances, j'avais déjà décidé de laisser de coté les services WCF pour faire communiquer Silverlight avec mes services webs.
Actuellement, j'utilise une solution à base de javascript, de serialisation json en ayant implémenté quelques fonctions génériques qui simplifient bien la vie. Si j'ai un temps de temps, je décrirais cette méthode en détails dans un prochain article.


AddThis Social Bookmark Button
 
Plus d'articles...
Suivez moi :
Facebook: damien.hoffschir del.icio.us: Filimindji FriendFeed: Filimindji LastFM: Filimindji Linked In: hoffschir Picasa: Filimindji Twitter: Filimindji YouTube: Filimindji
A voir aussi :