Wilfried Woivré & .Net

Partager des classes métiers entre plusieurs services WCF

JUIN17

Lorsque vous développez des applications Silverlight ou pour Windows Phone 7, il est très courant d’utiliser des Web Services en WCF afin d’exposer nos données métiers depuis les différents serveurs qui les héberge (sous Azure, c’est mieux ^^)

Vous pouvez concevoir vos accès aux données de plusieurs manières avec WCF, par exemple :

  • Un seul service WCF qui regroupera toutes les méthodes exposées
  • Plusieurs services WCF qui se répartissent les méthodes selon des critères de fonctionnalités (Authentification, Processus Métier A, Processus Métier B …)

On peut voir que le premier cas est très bien pour un effet “démo” ou une petite application, alors que le deuxième cas apporte une structure logique au web service et pour ne rien gâcher permet de répartir la charge entre les différents services WCF.

Bon cependant, vu qu’on n’est plus dans une démonstration, il y a des cas auxquels on ne pense pas de suite, par exemple, un Service A peut devoir utiliser les mêmes classes métiers qu’un Service B tous les deux référencés dans une même application.

Si l’on effectue, un ajout de service standard, on va faire simplement un clic droit sur le projet auquel on veut ajouter notre service, puis un “Add Service Reference”, on obtient une fenêtre telle que celle-ci

image

On va donc pouvoir ajouter nos deux services de la même façon.

J’ai donc reproduit dans mon application le cas dont je parlais, mes deux services utilisent ces contrats réciproquement

using System.ServiceModel;
using WCFSharedClass.Model;

namespace WCFSharedClass.Web
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IService1" in both code and config file together.
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        Class1 DoWork();

        [OperationContract]
        Class2 DoWork2();
    }
}
using System.ServiceModel;
using WCFSharedClass.Model;

namespace WCFSharedClass.Web
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IService2" in both code and config file together.
    [ServiceContract]
    public interface IService2
    {
        [OperationContract]
        void DoWork3(Class1 class1, Class2 class2);
    }
}

On voit donc que mes deux services utilisent les classes Class1 et Class2, on a donc dans mon application cliente (ici Windows Phone 7.0) ce code pour appeler nos services

private Service1.Class1 _class1;
private Service1.Class2 _class2;

private void Service1()
{
    var service1Client = new Service1.Service1Client();
    service1Client.DoWorkCompleted += (sender, e) =>
                                          {
                                              _class1 = e.Result;
                                              MessageBox.Show("Service 1 - DoWork");
                                          };
    service1Client.DoWorkAsync();


    service1Client = new Service1.Service1Client();
    service1Client.DoWork2Completed += (sender, e) =>
                                           {
                                               _class2 = e.Result;
                                               MessageBox.Show("Service 1 - DoWork2");
                                           };
    service1Client.DoWork2Async();
}

private void Service2()
{
    var service2Client = new Service2.Service2Client();
    service2Client.DoWork3Completed += (sender, e) => MessageBox.Show("Service 2 - DoWork3");
    service2Client.DoWork3Async(new Service2.Class1() { Value = _class1.Value }, new Service2.Class2() { Value = _class2.Value });
}

 

J’ai mis en gras les différentes instances de Class1 et Class2 et on peut voir que lorsqu’on utilise le Service1, la Class1 se situe dans le namespace Service1.Class1 alors que dans l’autre service elle se trouve dans Service2.Class1. On voit donc que pour appeler le Service2.DoWork3 avec les différents résultats du Service1 on est obligé de reconstruire les différents objets dont on a besoin. Même si ici, on n’a qu’un seul champ, on voit que ce n’est pas pratique. Le mieux est donc d’utiliser les mêmes Class1 et Class2 dans tous nos services !

Pour faire cela, il faut commencer par créer une bibliothèque de classe du côté client donc ici Windows Phone 7, et créer nos deux class de Model avec des propriétés publiques égales à celle exposées par nos Services, on a donc deux choix, le premier est une copie strictement identique, en ajoutant un fichier existant comme lien, comme on peut le voir ci dessous

image

 

L’avantage de faire ainsi, c’est que le code sera strictement identique du côté client, comme du côté serveur, cependant, si on utilise un objet côté serveur qui n’existe pas côté Windows Phone, on ne pourra pas compiler, mais vous pouvez facilement éviter cela avec des classes partielles.

L’autre technique est de copier séparément les fichiers comme j’ai fait pour la Class2, vous pouvez ainsi les modifier tant que les propriétés publiques exposées existes toujours

Côté Serveur :

using System.Runtime.Serialization;

namespace WCFSharedClass.Model
{
    [DataContract]
    public class Class2
    {
        [DataMember]
        public string Value { get; set; }
    }
}

Côté client :

namespace WCFSharedClass.Model
{
    public class Class2
    {
        private string _value;
        public string Value
        {
            get { return _value; }
            set { _value = value; }
        }
        public override bool Equals(object obj)
        {
            if (obj is Class2)
            {
                return Value == ((Class2) obj).Value;
            }
            return false;
        }

        public override int GetHashCode()
        {
            return this.Value.GetHashCode();
        }
    }
}

Note dans Visual Studio, on peut facilement voir qu’une classe est ajoutée comme lien, grâce au petit icône à côté de celle-ci.

image

Maintenant il suffit de reconfigurer vos services en utilisant votre assembly  contenant vos différentes classes.

image

Il vous suffit donc de faire cela pour tous vos services, les régénérer, compiler et recommencer si Visual Studio est grincheux ….

Et voilà donc le code final :

private Class1 _class1;
private Class2 _class2;

private void Service1()
{
    var service1Client = new Service1.Service1Client();
    service1Client.DoWorkCompleted += (sender, e) =>
                                          {
                                              _class1 = e.Result;
                                              MessageBox.Show("Service 1 - DoWork");
                                          };
    service1Client.DoWorkAsync();


    service1Client = new Service1.Service1Client();
    service1Client.DoWork2Completed += (sender, e) =>
                                           {
                                               _class2 = e.Result;
                                               MessageBox.Show("Service 1 - DoWork2");
                                           };
    service1Client.DoWork2Async();
}

private void Service2()
{
    var service2Client = new Service2.Service2Client();
    service2Client.DoWork3Completed += (sender, e) => MessageBox.Show("Service 2 - DoWork3");
    service2Client.DoWork3Async(_class1, _class2);
}
On utilise donc les mêmes classes pour nos deux services, ce qui est tout de même plus pratique ! 
A noter que vous avez à la fenêtre de configuration avancée via le bouton “Advanced” dans l’enregistrement du service !
Je ne vous fournis pas le code, tout est là ! 
Remonter

Utiliser RIA Services & le Table Storage d’Azure

MAI29

Lors d’un Azure Camp organisé par ZeCloud, j’ai montré comment exposer le Table Storage de Windows Azure via un WCF Data Services, cela nous permettait d’avoir une exposition de nos données via OData. Vous pouvez retrouver la démonstration sur le codeplex de ZeCloud, et me demander plus d’infos au prochain Azure Camp

 

Dans la même idée, je me suis aperçu que la dernière version de RIA Services proposait quelque chose du même genre, via son toolkit, on va donc voir comment le mettre en place !

 

Commençons déjà par créer un projet de type Cloud, ainsi qu’une application Silverlight avec un site web et WCF RIA Services. Il nous faut ensuite ajouter les références, par NuGet c’est plus facile

image

 

 

Maintenant, il nous faut créer notre Model, pour cela, on va prendre un cas très simple :

 

public class Person : TableEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    /// <summary>
    /// The property is set to be mentioned explicitly in the DataForm ...
    /// ONLY FOR THE DEMO
    /// </summary>
    public string MyPartitionKey
    {
        get
        {
            return base.PartitionKey;
        }
        set
        {
            base.PartitionKey = value;
        }
    }
}

On peut voir déjà quelques différences, premièrement on n’hérite pas de TableStorageEntity, mais de TableEntity qui hérite lui même de TableServiceEntity, et la deuxième c’est que pour le cas de la démo, j’ai voulu tester plusieurs PartitionKey, j’ai donc réexposé via une autre propriété celle ci afin qu’elle apparaisse dans mon DataForm Silverlight.

 

Maintenant, voyons notre contexte de données pour notre Table Storage

public class AzureServiceContext : TableEntityContext
{      
    public AzureServiceContext() : 
        base(RoleEnvironment
            .GetConfigurationSettingValue("Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString"))
    {
    }

    public TableEntitySet<Person> People
    {
        get { return base.GetEntitySet<Person>(); }
    }
}

 

Donc de même ici, on peut voir quelques différences, déjà au niveau de l’héritage, ici on hérite de TableEntityContext qui hérite lui même de TableServiceEntity.

De plus, on peut voir que l’on ne gère pas non plus la création des tables dans le Table Storage, vu que le toolkit de RIA Services s’en occupe pour nous.

Il ne vous reste plus qu’à créer votre Domain Service de façon classique, il faut juste renseigner aucun contexte.

image

 

Maintenant, implémentons notre DomainService

[EnableClientAccess()]
public class TSDomainService : TableDomainService<AzureServiceContext>
{

    protected override string PartitionKey
    {
        get
        {
            return null;
        }
    }

    public IQueryable<Person> GetPeople()
    {
        return EntityContext.People;
    }

    public void AddPerson(Person person)
    {
        EntityContext.People.Add(person);
    }

    public void DeletePerson(Person person)
    {
        EntityContext.People.Delete(person);
    }

    public void UpdatePerson(Person person)
    {
        EntityContext.People.Update(person);
    }
}

On a dorénavant la possibilité de faire un TableDomainService pour englober notre contexte Azure, de même les méthodes standards de CRUD sont facilitées.

Voyons maintenant la PartitionKey, par défaut  le toolkit met la PartitionKey à la valeur du nom du Domain Service, pour éviter qu’elle soit définit ainsi, il suffit de surcharger la PartitionKey, cependant cela veut dire qu’il vous faudra la spécifier à chaque fois, ce qui est mieux si vous voulez une bonne structure de donnée dans votre Table Storage

 

Et voilà le résultat dans un DataForm Silverlight

image

 

Vous pouvez retrouver les sources de la solution ici

Remonter

Silverlight 5 : Arrivée de l’Ancestor RelativeSource Binding

AVRI13

Et oui, le voilà, comme quoi, on n’arrête pas le progrès dans Silverlight !

Une petite démonstration sur l’ancestor RelativeSource Binding pour commencer cette soirée ….

Je vous ai montré à plusieurs reprises comment lier vos commandes ou vos actions de vos datatemplate compris dans des ItemsControl à des méthodes de vos ViewModel !

 

Vous avez donc accès à une solution qui pouvait être amélioré mais qui était tout de même bien pratique, vous pouvez la retrouver ici ou en vidéo.

 

On va déjà commencer par créer une application en Silverlight 5 !

image

Ensuite on va créer notre ViewModel de façon on ne peut plus classique

public class MainViewModel : ViewModelBase
{
    private readonly ObservableCollection<Person> _people = new ObservableCollection<Person>();
    public ObservableCollection<Person> People
    {
        get { return _people; }
    }

    private ICommand _callCommand;
    public ICommand CallCommand
    {
        get { return _callCommand ?? (_callCommand = new RelayCommand<Person>(Call)); }
    }

    public MainViewModel()
    {
        People.Add(new Person() { FirstName = "Wilfried", LastName = "Woivré" });
        People.Add(new Person() { FirstName = "Harry", LastName = "Cover" });
    }

    public void Call(Person person)
    {
        MessageBox.Show(string.Format("Call Person : {0} {1}", person.FirstName, person.LastName));
    }

    public void Call()
    {
        MessageBox.Show("Call Method");
    }
}

On lie correctement notre Vue à notre ViewModel, comme cela :

public partial class MainPage : UserControl
{
    public MainPage()
    {
        InitializeComponent();
        this.Loaded += (sender, e) => this.DataContext = new MainViewModel();
    }
}

Et maintenant passons à la vue, puisque le model c’est juste deux propriétés ….

    <Grid x:Name="LayoutRoot" Background="White">

        <ListBox ItemsSource="{Binding People}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Button Content="CallMethodAction" Margin="5"  >
                            <i:Interaction.Triggers>
                                <i:EventTrigger EventName="Click">
                                    <ei:CallMethodAction MethodName="Call" TargetObject="{Binding DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}" />
                                </i:EventTrigger>
                            </i:Interaction.Triggers>
                        </Button>
                        <Button Content="Command" Margin="5" >
                            <i:Interaction.Triggers>
                                <i:EventTrigger EventName="Click">
                                    <i:InvokeCommandAction 
                                        Command="{Binding DataContext.CallCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" 
                                        CommandParameter="{Binding}"/>
                                </i:EventTrigger>
                            </i:Interaction.Triggers>
                        </Button>
                        <TextBlock Text="{Binding FirstName}" Margin="5" />
                        <TextBlock Text="{Binding LastName}" Margin="5" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

 

Et donc quelle est la principale différence par rapport à avant, c’est l’apparition de RelativeSource={RelativeSource AncestorType=UserControl} , cela permet d’appeler notre command ou notre méthode selon le DataContext lié à notre UserControl, nous n’avons donc plus besoin de passer via une méthode dans une classe partielle du model !

 

Vous pouvez retrouver les sources de la solution ici

Pour plus d’infos sur les autres nouveautés de Silverlight 5, je vous conseille le blog de Tim Heuer

Remonter

Silverlight 5 Beta disponible

AVRI13

Voilà si vous avez loupé cette annonce au MIX, Silverlight 5 est disponible ainsi que le Blend qui va avec !

 

Silverlight 5 => Silverlight 5 Beta Tools for Visual Studio SP1

Expression Blend 5 => Expression Blend Preview for Silverlight 5

 

Voilà il vous faudra Visual Studio 2010 SP1 pour pouvoir l’installer !

Et si vous voulez revivre le MIX, c’est à cette adresse => http://live.visitmix.com/

Remonter

Afficher une énumération avec des attributs personnalisés dans Silverlight

MARS8

Dans différents projets, il est souvent utile d’utiliser des énumérations pour gérer des choix qui sont fixés à l’avance et qui sont voués à ne jamais changer. Par exemple, le choix Masculin/Féminin pour un formulaire d’inscription.

 

Pour commencer, on va utilisé une énumération simple, on lui rajoutera un attribut personnalisé par la suite, on va donc créer dans notre application une énumération de ce type :

public enum EnumValue
{
    EnumValue1 = 0, 
    EnumValue2 = 1,
    EnumValue3 = 2,
    EnumValue4 = 3
}

Maintenant si voyons comment afficher cette liste dans une ListBox Silverlight, commençons par le code behind de notre page.

private readonly ObservableCollection<EnumValue> _values = new ObservableCollection<EnumValue>();
public ObservableCollection<EnumValue> Values
{
    get { return _values; }
}

private EnumValue _selectedValue;
public EnumValue SelectedValue
{
    get { return _selectedValue; }
    set
    {
        _selectedValue = value;
        OnPropertyChanged("SelectedValue");
    }
}

public MainPage()
{
    InitializeComponent();
    this.Loaded += MainPage_Loaded;   
}

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    Values.Add(EnumValue.EnumValue1);
    Values.Add(EnumValue.EnumValue2);
    Values.Add(EnumValue.EnumValue3);
    Values.Add(EnumValue.EnumValue4);
    this.DataContext = this;
}

public event PropertyChangedEventHandler PropertyChanged;

public void OnPropertyChanged(string propertyName)
{
    if (String.IsNullOrWhiteSpace(propertyName))
        return;

    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

On remarque qu’on est obligé d’ajouter toutes nos valeurs à la main vu que la méthode GetValues() n’existe pas pour les enums dans Silverlight.

On a donc une interface de ce type, à gauche notre ListBox, et à notre droite, notre item sélectionné.

image

Bon on peut voir avec cette première version que ça marche, cependant ceci demande de rajouter tous les champs à la main, et de déclarer à la fois une liste pour les différentes valeurs, et un champ pour l’item sélectionné.

Commençons donc par nous affranchir de l’ajout des champs à la main, pour cela on va utiliser la réflexion, cela nous donne donc ceci :

var list = from item in typeof (EnumValue).GetFields()
           where item.IsLiteral
           select (EnumValue)Enum.Parse(typeof(EnumValue), item.Name, true);

foreach (var enumValue in list)
{
    Values.Add(enumValue);
}

Bon c’est déjà pas mal, on a évité l’ajout de code, lors de l’ajout d’une valeur. Cependant, cette opération va être effectuer à chaque appel de notre méthode Load, et la liste n’a aucune pertinence à se trouver ici, puisque finalement ce n’est qu’une source de données comme une autre, elle est juste liée à notre type d’énumération.

 

On va donc extraire nos données dans un cache de ce type.

public class EnumCache
{
    private static readonly IDictionary<Type, Object[]> Cache = new Dictionary<Type, Object[]>();

    public static Object[] GetValues(Type type)
    {
        if (!type.IsEnum)
            throw new ArgumentException("Type '" + type.Name + "' is not an enum");

        Object[] values;
        if (!Cache.TryGetValue(type, out values))
        {
            values = (from item in type.GetFields()
                      where item.IsLiteral
                      select Enum.Parse(type, item.Name, true)).ToArray();
            Cache[type] = values;
        }
        return values;
    }
}

On modifie ainsi notre remplissage de liste :

var list = EnumCache.GetValues(typeof (EnumValue));

foreach (var enumValue in list)
{
    Values.Add((EnumValue)Enum.Parse(typeof(EnumValue), enumValue.ToString(), true));
}

 

L’avantage de ce cache, est que l’on peut stocker n’importe quel type d’énumération, et le traitement est fait une seule fois à l’appel de notre méthode GetValues(), puis est stocké en cache pour de futures utilisations.

 

Maintenant qu’on a fait cela, le but serait de s’affranchir de notre déclaration de liste, pour cela on peut utiliser un Converter sur notre Listbox, vu qu’on a uniquement besoin du type de notre enum.

Notre converter est donc le suivant

 

public class MyEnumListConverter : IValueConverter
{

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value is EnumValue)
            return EnumCache.GetValues(typeof(EnumValue));

        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Et la déclaration de notre ListBox est donc la suivante !

        <ListBox 
            ItemsSource="{Binding SelectedValue, Converter={StaticResource myEnumValueConverter}}" 
            SelectedItem="{Binding SelectedValue, Mode=TwoWay}" 
            Margin="0,0,221,0" />

 

Ainsi on peut totalement s’affranchir de notre déclaration de notre ObservableCollection, et uniquement se basé sur notre champ sélectionné.

 

Bon maintenant, c’est bien, mais bon le soucis c’est que les valeurs affichées sont uniquement les différentes résultats des méthodes ToString() de chaque élément de l’énumération.

Ce qu’on peut envisager de faire, c’est d’ajouter pour chaque valeur de l’énumération une image et un texte associé. Pour cela on va donc créer un attribut de type custom.

Commençons donc par définir notre attribut personnalisé, on aurait pu utiliser un Attribut pré-existant dans le framework :

public class EnumValueAttribute : Attribute
{
    public string Text { get; set; }
    public Uri ImageUrl { get; set; }

    public EnumValueAttribute(string text)
    {
        this.Text = text;
    }

    public EnumValueAttribute(string text, string imageUrl)
    {
        this.Text = text;
        this.ImageUrl = new Uri(imageUrl, UriKind.Relative);
    }
}

 

Maintenant que nous l’avons créé, il faut l’utiliser dans notre énumération :

public enum EnumValue
{
    [EnumValue("Item 1")]
    EnumValue1 = 0, 
    [EnumValue("Item 2")]
    EnumValue2 = 1,
    [EnumValue("Item 3")]
    EnumValue3 = 2,
    [EnumValue("Item 4", "/VS.jpg")]
    EnumValue4 = 3
}

Il faut noter que pour cette syntaxe, il faut que l’image soit en mode “Content” et “Copy if never” !

Bon maintenant il va nous falloir notre méthode de Cache, vu qu’on veut récupérer l’attribut et non la valeur de notre champ.

public class EnumCache
{
    private static readonly IDictionary<Type, Object[]> Cache = new Dictionary<Type, Object[]>();

    public static Object[] GetValues(Type type)
    {
        if (!type.IsEnum)
            throw new ArgumentException("Type '" + type.Name + "' is not an enum");

        Object[] values;
        if (!Cache.TryGetValue(type, out values))
        {
            values = (from item in type.GetFields()
                      where item.IsLiteral
                      select ((EnumValueAttribute)item.GetCustomAttributes(typeof(EnumValueAttribute), false).First())).ToArray();
            Cache[type] = values;
        }
        return values;
    }
}

 

On va ensuite créer un converter pour notre item sélectionné, ainsi que pour la définition via le code behind. On aura donc en suivant le même principe le code suivant

public class MyEnumValueConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return value;
        if (value is EnumValue)
        {
            var enumValueAttribute = from e in typeof(EnumValue).GetFields()
                                     where e.IsLiteral && e.Name == ((EnumValue)value).ToString()
                                     select ((EnumValueAttribute)e.GetCustomAttributes(typeof(EnumValueAttribute), false).First());

            return enumValueAttribute.FirstOrDefault();
        }

        return value;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null)
            return value;

        var valueAttribute = value as EnumValueAttribute;

        var elt = (from f in typeof (EnumValue).GetFields()
                   let attribute =
                       ((EnumValueAttribute) f.GetCustomAttributes(typeof (EnumValueAttribute), false).FirstOrDefault())
                   where f.IsLiteral
                         && attribute.Text == valueAttribute.Text
                         && attribute.ImageUrl == valueAttribute.ImageUrl
                   select (EnumValue) Enum.Parse(typeof (EnumValue), f.Name, false)).FirstOrDefault();

        return elt;
    }
}

On peut noter que j’aurais pu redéfinir le GetHashCode, et le Equals de mon attribut afin d’éviter de faire la vérification dans la requête Linq !

Et il nous suffit d’ajuster notre code XAML en adéquation avec nos modifications.

        <ListBox 
            ItemsSource="{Binding SelectedValue, Converter={StaticResource myEnumListConverter}}" 
            SelectedItem="{Binding SelectedValue, Mode=TwoWay, Converter={StaticResource myEnumValueConverter}}" 
            Margin="0,0,221,0">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal" 
                                Height="40">
                        <Image Source="{Binding ImageUrl}" 
                               Margin="5" 
                               Height="30"/>
                        <TextBlock Text="{Binding Text}" />
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        
        <TextBlock Height="23" 
                   HorizontalAlignment="Left" 
                   Margin="200,96,0,0"
                   Text="Selected Item" 
                   VerticalAlignment="Top" />
        <ContentControl Height="40" 
                        HorizontalAlignment="Left" 
                        Margin="200,125,0,0" 
                        DataContext="{Binding SelectedValue, Converter={StaticResource myEnumValueConverter}}" 
                        VerticalAlignment="Top" 
                        Width="152">
            <StackPanel Orientation="Horizontal" 
                        Height="40">
                <Image Source="{Binding ImageUrl}" 
                       Margin="5" 
                       Height="30"/>
                <TextBlock Text="{Binding Text}" />
            </StackPanel>
        </ContentControl>

Donc rien de bien compliqué dans notre vue, uniquement un template pour notre listbox.

Et pour finir, ça nous donne donc un rendu de ce type :

image

Voilà en tout cas, ça peut être pratique pour plein de développement avec des enums, et des données un peu customisables, comme de la localisation (DisplayAttribute) ou des images.

Bon, bien entendu, comme je pense à vous, je vous fournis le code source final ici.

Remonter