Discussion autour de la réflexion

Introduction

La réflexion est un outil puissant, mais il est important de comprendre dans quel cas l’utiliser. En effet, une mauvaise utilisation peut lourdement pénaliser un projet.

On remarque qu’en pratique, elle n’est pas beaucoup utilisé, en particulier pour les raisons suivantes :
– un code utilisant la réflexion demande des connaissances particulières non triviales pour être maintenu et évolué (code moins lisible et moins maintenable).
– le code est plus difficile à refactorer, (effets de bords peu prévisibles).
– une mauvaise utilisation peut rapidement entrainer des performances catastrophiques.

Utiliser la réflexion n’est pas une fin en soi, ce n’est pas un style de programmation.
Il ne faut donc pas utiliser cette technique à tort ou à travers, mais repérer les (rares) cas pour lesquels elle peut s’avérer utile.

Deux exemples classiques d’utilisation de la reflexion : l’ajout de traces (log4net), la décompilation (ILSpy).

Je vous propose le projet suivant :

Nous voudrions disposer d’une fenêtre qui prend en paramètre un objet et lorsqu’elle s’ouvre, qu’elle génère automatiquement tous les contrôles nécessaires pour le modifier au RunTime quelque soit son type.
Évidement lors de la fermeture de la fenêtre, nous voulons que les modifications soient prises en compte.

J’ai choisi d’utiliser le Framework 3.5 car c’est la version la plus utilisée au moment où j’écris ces lignes.

Conception

Nous allons diviser le projet en deux parties :

La partie ‘métier’, que nous nommerons « ObjectModifier », et la fenêtre qui gèrera la création des contrôles et leur affichage.

L’interface IObjectModifier:

Voici l’interface de notre objet :

using System;
using System.Collections.Generic;
 
namespace ObjectExplorer.ObjectModifier
{
    public interface IObjectModifier
    {
        List getListProperty();
        Boolean setValue(String name, Object value);
    }
}

L’objet métier

Voici un exemple d’implémentation de cette interface :

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
 
 
namespace ObjectModifier
{
    class BasicObjectModifier : IObjectModifier
    { 
        private readonly Object source;
        private List<PropertyInfo> listCouple;
        private readonly string enumerablePrefix = "elementOfTheEnumerable_";
 
        public BasicObjectModifier(Object o)
        {
            source = o;
            Initialise();
        }
        public BasicObjectModifier(Object o, string enumerablePrefix)
        {
            this.source = o;
            this.enumerablePrefix = enumerablePrefix;
            Initialise();
        }
 
        //Initialisation de la liste des propriétés de l'objet qu'on peut modifier.
        private void Initialise()
        {
            var currentType = source.GetType();
            listCouple = new List<PropertyInfo>();
            List<PropertyInfo> listProperties = currentType.GetProperties().ToList();
            foreach (var property in listProperties)
            {
                if (property.CanWrite)
                {
                    listCouple.Add(property);
                }
            }
        }
 
        #region Implémentation de l'interface
 
        public List<CoupleAttributValue> getListProperty()
        {
            //D'un objet que l'on voudra modifier ...
            List<CoupleAttributValue> result = new List<CoupleAttributValue>();
            //1-Ses propriétés
            foreach (var propertyInfo in listCouple)
                try
                {
                    CoupleAttributValue currentCouple = new CoupleAttributValue(propertyInfo.Name,
                                                                                propertyInfo.GetValue(source, null),
                                                                                propertyInfo.PropertyType);
 
                    result.Add(currentCouple);
                }
                catch
                {
                }
            //2-Si l'objet est une liste, ses éléments.
            int i = 0;
            if (source is IEnumerable)
            {
                foreach (object o in (source as IEnumerable))
                {
                    //Ici on pourra modifier les éléments de la liste si ce sont des objets,
                    //mais pas modifier les éléments composant cette liste.
                    //(Par exemple une liste de String sera inchangeable car l'object String est immuable)
                    CoupleAttributValue currentCouple = new CoupleAttributValue(enumerablePrefix + i, o, typeof(Object));
                    result.Add(currentCouple);
                    i++;
                }
            }
            return result;
        }
        public Boolean setValue(String name, Object value)
        {
            try
            {
                //Utilisation banale de la reflexion.
                source.GetType().GetProperty(name).SetValue(source, value, null);
            }
            catch (ArgumentException)
            {
                return false;
            }
            return true;
        }
        #endregion
    }
 
    public class CoupleAttributValue
    {
        public String CoupleAttributeName { get; set; }
        public Object CoupleValue { get; set; }
        public Type CoupleType { get; private set; }
 
        public CoupleAttributValue(string attributeName, Object value, Type type)
        {
            CoupleValue = value;
            CoupleAttributeName = attributeName;
            CoupleType = type;
        }
    }
}

L’interface graphique

Un peu poussée car elle doit créer les contrôles et s’abonner aux événements :

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
 
namespace ObjectModifier
{
    /// &lt;summary&gt;
    /// Interaction logic for ObjectModifierByReflexion.xaml
    /// &lt;/summary&gt;
    public partial class ObjectModifierByReflexion : Window
    {
        public Object input;
 
        private readonly
 ObservableCollection<CoupleAttributValueForBinding> propertiesCollection = new ObservableCollection<CoupleAttributValueForBinding>();
 
        public ObservableCollection<CoupleAttributValueForBinding> PropertiesCollection
        { get { return propertiesCollection; } }
 
        public ObjectModifierByReflexion(Object toModify)
        {
 
            IObjectModifier toObserve;
            input = toModify;
            //Si on a un objet, on initialise la liste à afficher...
            if (input != null)
            {
                toObserve = new BasicObjectModifier(input);
 
                List<CoupleAttributValue> listProperties =
 toObserve.getListProperty();
                foreach (CoupleAttributValue couple in listProperties)
                {
                    propertiesCollection.Add(new
 CoupleAttributValueForBinding(toObserve, couple));
                }
            }
            InitializeComponent();
        }
    }
 
    //Cette classe nous sert à transformer automatiquement un CoupleAttributValue, en un objet facilement utilisable
    //dans notre interface graphique. En particulier, cet objet génère le control approprié selon le type de la propriété
    //Du CoupleAttributValue passé dans le constructeur.
    public class CoupleAttributValueForBinding
    {
        private readonly IObjectModifier initialObject;
 
        public String AttributeName { get; set; }
        public Object Value { get; set; }
        public Type CurrentType { get; set; }
        public Control Action
        {
            get
            {
                return GetcontrolFromType();
            }
        }
        public CoupleAttributValueForBinding(IObjectModifier
 initialObject, CoupleAttributValue couple)
        {
            this.initialObject = initialObject;
            Value = couple.CoupleValue;
            AttributeName = couple.CoupleAttributeName;
            CurrentType = couple.CoupleType;
        }
 
        #region Selection du control et définition des événements
        private Control GetcontrolFromType()
        {
            var currentTypeCode = getCurrentType();
            switch (currentTypeCode)
            {
                case TypeCode.Boolean:
                    var myCheckBox = new CheckBox
                    {
                        IsChecked =
                            (Boolean)Value
                    };
                    myCheckBox.Name = AttributeName;
                    myCheckBox.Checked += CheckBoxCheckedChanged;
                    myCheckBox.Unchecked += CheckBoxCheckedChanged;
                    return myCheckBox;
                case TypeCode.String:
                    var myTextBox = new TextBox
                    {
                        Text = Value !=
                            null ? Value.ToString() : "test"
                    };
                    myTextBox.Name = AttributeName;
                    myTextBox.LostFocus += TextBoxStringFocusLost;
                    return myTextBox;
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                    var myTextBox2 = new TextBox
                    {
                        Text = Value !=
                            null ? Value.ToString() : "0"
                    };
                    myTextBox2.Name = AttributeName;
                    myTextBox2.LostFocus += TextBoxIntFocusLost;
                    return myTextBox2;
                case TypeCode.Double:
                    var myTextBox3 = new TextBox
                    {
                        Text = Value !=
                            null ? Value.ToString() : "0"
                    };
                    myTextBox3.Name = AttributeName;
                    myTextBox3.LostFocus += TextBoxDoubleFocusLost;
                    return myTextBox3;
                case TypeCode.Decimal:
                    var myTextBox4 = new TextBox
                    {
                        Text = Value !=
                            null ? Value.ToString() : "0"
                    };
                    myTextBox4.Name = AttributeName;
                    myTextBox4.LostFocus += TextBoxDecimalFocusLost;
                    return myTextBox4;
                case TypeCode.DateTime:
                    var myTextBoxDate = new TextBox
                    {
                        Text = Value !=
                            null ? Value.ToString() : "test"
                    };
                    myTextBoxDate.Name = AttributeName;
                    myTextBoxDate.LostFocus += TextBoxDateFocusLost;
                    return myTextBoxDate;
                case TypeCode.Object:
                    if (Value != null)
                    {
                        var myButton = new Button { Content = Value };
                        myButton.Name = AttributeName;
                        myButton.Click += ButtonClick;
                        return myButton;
                    }
                    else
                    {
                        return new Label { Content = "Objet null" };
                    }
                default:
                    return new Label { Content = "Rien a faire!" };
            }
        }
 
        private TypeCode getCurrentType()
        {
            var currentTypeCode = Type.GetTypeCode(CurrentType);
            switch (currentTypeCode)
            {
                case TypeCode.Object:
                    if (CurrentType == typeof(Nullable&lt;DateTime&gt;))
                    {
                        return TypeCode.DateTime;
                    }
                    else
                    {
                        return currentTypeCode;
                    }                    
                default:
                    return currentTypeCode;
            }
        }
 
        private void CheckBoxCheckedChanged(object sender, EventArgs
 e)
        {
            var myCheckBox = (CheckBox)sender;
            Boolean? value = myCheckBox.IsChecked;
            if (value != null)
            {
                initialObject.setValue(myCheckBox.Name, value);
            }
 
        }
        private static void ButtonClick(object sender, EventArgs e)
        {
            var myButton = (Button)sender;
            Object value = myButton.Content;
            if (value != null)
            {
                try
                {
                    ObjectModifierByReflexion windows = new
 ObjectModifierByReflexion(value);
                    windows.ShowDialog();
                }
                catch (Exception)
                {
                    myButton.Content = "Exception récupérée";
                    myButton.IsEnabled = false;
                }
            }
        }
        private void TextBoxStringFocusLost(object sender, EventArgs e)
        {
            var myTextBox = (TextBox)sender;
            String value = myTextBox.Text;
            if (value != null)
            {
                initialObject.setValue(myTextBox.Name, value);
            }
 
        }
        private void TextBoxIntFocusLost(object sender, EventArgs e)
        {
            var myTextBox = (TextBox)sender;
 
            try
            {
                int value = Int32.Parse(myTextBox.Text);
                initialObject.setValue(myTextBox.Name, value);
            }
            catch (Exception)
            {
                ;
            }
        }
        private void TextBoxDecimalFocusLost(object sender, EventArgs
 e)
        {
            var myTextBox = (TextBox)sender;
 
            try
            {
                decimal value = Decimal.Parse(myTextBox.Text);
                initialObject.setValue(myTextBox.Name, value);
            }
            catch (Exception)
            {
                ;
            }
        }
        private void TextBoxDoubleFocusLost(object sender, EventArgs
 e)
        {
            var myTextBox = (TextBox)sender;
 
            try
            {
                double value = Double.Parse(myTextBox.Text);
                initialObject.setValue(myTextBox.Name, value);
            }
            catch (Exception)
            {
                ;
            }
        }
        private void TextBoxDateFocusLost(object sender, EventArgs e)
        {
            var myTextBox = (TextBox)sender;
 
            try
            {
                DateTime value;
                if (DateTime.TryParse(myTextBox.Text, out value))
                {
                    initialObject.setValue(myTextBox.Name, value);
                }
            }
            catch (Exception) { }
        }
 
        #endregion
 
    }
}

 

Xaml associé
<Window x:Class="ObjectModifier.ObjectModifierByReflexion"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
    Title="Change your object by reflexion" Height="319" Width="776">
    <ListView Name="mainListView" ItemsSource="{Binding PropertiesCollection}">
        <ListView.View>
            <GridView>
                <GridView.Columns>
                    <GridViewColumn Header="Property" Width="150">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Label Content="{Binding AttributeName}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Value" Width="350">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Label Content="{Binding Value}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Type" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Label Content="{Binding CurrentType}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                    <GridViewColumn Header="Action" Width="150">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <ContentControl Content="{Binding Action}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView.Columns>
            </GridView>
        </ListView.View>
    </ListView>
</Window>

Comment instancier ma fenêtre?

Pour être adopté par tous, un outil doit être simplissime, je pense qu’il l’est.

try
{
    ObjectModifierByReflexion windows = new ObjectModifierByReflexion(yourObject);
    windows.ShowDialog();
}
catch (Exception ex)
{
    addLog("Exception récupérée : " + ex);
}
Articles à lire

http://codingly.com/2008/05/02/optimisation-des-invocations-dynamiques-de-methodes-en-c/
http://marlongrech.wordpress.com/2008/02/28/gridviewcolumn-displaymemberbinding-vs-celltemplate/

Comments are closed.