Comment créer des propriétés dynamiques en C #?

88

Je cherche un moyen de créer une classe avec un ensemble de propriétés statiques. Au moment de l'exécution, je souhaite pouvoir ajouter d'autres propriétés dynamiques à cet objet à partir de la base de données. Je voudrais également ajouter des capacités de tri et de filtrage à ces objets.

Comment faire cela en C #?

Eatdoku
la source
3
Quel est le but de ce cours? Votre demande me rend suspect que vous ayez vraiment besoin d'un modèle de conception ou de quelque chose, mais ne pas savoir quel est votre cas d'utilisation signifie que je n'ai pas vraiment de suggestion.
Brian

Réponses:

60

Vous pourriez utiliser un dictionnaire, par exemple

Dictionary<string,object> properties;

Je pense que dans la plupart des cas où quelque chose de similaire est fait, c'est fait comme ça.
Dans tous les cas, vous ne gagneriez rien à créer une propriété "réelle" avec des accesseurs set et get, car elle ne serait créée qu'au moment de l'exécution et vous ne l'utiliseriez pas dans votre code ...

Voici un exemple, montrant une implémentation possible du filtrage et du tri (pas de vérification d'erreur):

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1 {

    class ObjectWithProperties {
        Dictionary<string, object> properties = new Dictionary<string,object>();

        public object this[string name] {
            get { 
                if (properties.ContainsKey(name)){
                    return properties[name];
                }
                return null;
            }
            set {
                properties[name] = value;
            }
        }

    }

    class Comparer<T> : IComparer<ObjectWithProperties> where T : IComparable {

        string m_attributeName;

        public Comparer(string attributeName){
            m_attributeName = attributeName;
        }

        public int Compare(ObjectWithProperties x, ObjectWithProperties y) {
            return ((T)x[m_attributeName]).CompareTo((T)y[m_attributeName]);
        }

    }

    class Program {

        static void Main(string[] args) {

            // create some objects and fill a list
            var obj1 = new ObjectWithProperties();
            obj1["test"] = 100;
            var obj2 = new ObjectWithProperties();
            obj2["test"] = 200;
            var obj3 = new ObjectWithProperties();
            obj3["test"] = 150;
            var objects = new List<ObjectWithProperties>(new ObjectWithProperties[]{ obj1, obj2, obj3 });

            // filtering:
            Console.WriteLine("Filtering:");
            var filtered = from obj in objects
                         where (int)obj["test"] >= 150
                         select obj;
            foreach (var obj in filtered){
                Console.WriteLine(obj["test"]);
            }

            // sorting:
            Console.WriteLine("Sorting:");
            Comparer<int> c = new Comparer<int>("test");
            objects.Sort(c);
            foreach (var obj in objects) {
                Console.WriteLine(obj["test"]);
            }
        }

    }
}
Paolo Tedesco
la source
30

Si vous en avez besoin à des fins de liaison de données, vous pouvez le faire avec un modèle de descripteur personnalisé ... en implémentant ICustomTypeDescriptor, TypeDescriptionProvideret / ou TypeCoverter, vous pouvez créer vos propres PropertyDescriptorinstances au moment de l'exécution. C'est ce que les contrôles comme DataGridView, PropertyGridetc. utilisent pour afficher les propriétés.

Pour lier à des listes, vous aurez besoin de ITypedListet IList; pour le tri de base: IBindingList; pour le filtrage et le tri avancé: IBindingListView; pour le support complet de la "nouvelle ligne" ( DataGridView): ICancelAddNew(ouf!).

Il est beaucoup de travail cependant. DataTable(bien que je déteste ça) est un moyen bon marché de faire la même chose. Si vous n'avez pas besoin de liaison de données, utilisez simplement une table de hachage ;-p

Voici un exemple simple - mais vous pouvez faire beaucoup plus ...

Marc Gravell
la source
merci ... être capable de databind directement est ce que je recherchais. Donc, fondamentalement, la façon la moins chère de le faire est de traduire la collection d'objets en DataTable, puis de lier la table à la place. Je suppose qu'il y a plus de choses à craindre après la conversion aussi .. merci pour votre contribution.
Eatdoku
En remarque, la liaison de données via ICustomTypeDescriptor n'est pas prise en charge par Silverlight :(.
Curt Hagenlocher
En tant que nœud secondaire de la note d'accompagnement, Silverlight 5 a introduit l'interface ICustomTypeProvider à la place de ICustomTypeDescriptor. ICustomTypeProvider a ensuite été porté vers .NET Framework 4.5, pour permettre la portabilité entre Silverlight et .NET Framework. :).
Edward
12

Créez une table de hachage appelée "Propriétés" et ajoutez-y vos propriétés.

Aric TenEyck
la source
12

Je ne suis pas sûr que vous vouliez vraiment faire ce que vous dites vouloir faire , mais ce n'est pas à moi de raisonner pourquoi!

Vous ne pouvez pas ajouter de propriétés à une classe après qu'elle a été JITed.

Le plus proche que vous pourriez obtenir serait de créer dynamiquement un sous-type avec Reflection.Emit et de copier les champs existants, mais vous devrez mettre à jour toutes les références à l'objet vous-même.

Vous ne pourrez pas non plus accéder à ces propriétés au moment de la compilation.

Quelque chose comme:

public class Dynamic
{
    public Dynamic Add<T>(string key, T value)
    {
        AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Dynamic.dll");
        TypeBuilder typeBuilder = moduleBuilder.DefineType(Guid.NewGuid().ToString());
        typeBuilder.SetParent(this.GetType());
        PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(key, PropertyAttributes.None, typeof(T), Type.EmptyTypes);

        MethodBuilder getMethodBuilder = typeBuilder.DefineMethod("get_" + key, MethodAttributes.Public, CallingConventions.HasThis, typeof(T), Type.EmptyTypes);
        ILGenerator getter = getMethodBuilder.GetILGenerator();
        getter.Emit(OpCodes.Ldarg_0);
        getter.Emit(OpCodes.Ldstr, key);
        getter.Emit(OpCodes.Callvirt, typeof(Dynamic).GetMethod("Get", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(typeof(T)));
        getter.Emit(OpCodes.Ret);
        propertyBuilder.SetGetMethod(getMethodBuilder);

        Type type = typeBuilder.CreateType();

        Dynamic child = (Dynamic)Activator.CreateInstance(type);
        child.dictionary = this.dictionary;
        dictionary.Add(key, value);
        return child;
    }

    protected T Get<T>(string key)
    {
        return (T)dictionary[key];
    }

    private Dictionary<string, object> dictionary = new Dictionary<string,object>();
}

Je n'ai pas installé VS sur cette machine, alors faites-moi savoir s'il y a des bugs massifs (enfin ... autres que les problèmes de performances massifs, mais je n'ai pas écrit la spécification!)

Vous pouvez maintenant l'utiliser:

Dynamic d = new Dynamic();
d = d.Add("MyProperty", 42);
Console.WriteLine(d.GetType().GetProperty("MyProperty").GetValue(d, null));

Vous pouvez également l'utiliser comme une propriété normale dans un langage qui prend en charge la liaison tardive (par exemple, VB.NET)

Alun Harford
la source
4

J'ai fait exactement cela avec une interface ICustomTypeDescriptor et un dictionnaire.

Implémentation d'ICustomTypeDescriptor pour les propriétés dynamiques:

J'ai récemment eu l'obligation de lier une vue de grille à un objet d'enregistrement qui pourrait avoir un nombre quelconque de propriétés pouvant être ajoutées et supprimées au moment de l'exécution. Il s'agissait de permettre à un utilisateur d'ajouter une nouvelle colonne à un jeu de résultats pour entrer un jeu de données supplémentaire.

Ceci peut être réalisé en ayant chaque «ligne» de données comme un dictionnaire avec la clé étant le nom de la propriété et la valeur étant une chaîne ou une classe qui peut stocker la valeur de la propriété pour la ligne spécifiée. Bien sûr, avoir une liste d'objets Dictionary ne pourra pas être lié à une grille. C'est là qu'intervient ICustomTypeDescriptor.

En créant une classe wrapper pour le dictionnaire et en le faisant adhérer à l'interface ICustomTypeDescriptor, le comportement de renvoi des propriétés d'un objet peut être remplacé.

Jetez un œil à l'implémentation de la classe de données 'row' ci-dessous:

/// <summary>
/// Class to manage test result row data functions
/// </summary>
public class TestResultRowWrapper : Dictionary<string, TestResultValue>, ICustomTypeDescriptor
{
    //- METHODS -----------------------------------------------------------------------------------------------------------------

    #region Methods

    /// <summary>
    /// Gets the Attributes for the object
    /// </summary>
    AttributeCollection ICustomTypeDescriptor.GetAttributes()
    {
        return new AttributeCollection(null);
    }

    /// <summary>
    /// Gets the Class name
    /// </summary>
    string ICustomTypeDescriptor.GetClassName()
    {
        return null;
    }

    /// <summary>
    /// Gets the component Name
    /// </summary>
    string ICustomTypeDescriptor.GetComponentName()
    {
        return null;
    }

    /// <summary>
    /// Gets the Type Converter
    /// </summary>
    TypeConverter ICustomTypeDescriptor.GetConverter()
    {
        return null;
    }

    /// <summary>
    /// Gets the Default Event
    /// </summary>
    /// <returns></returns>
    EventDescriptor ICustomTypeDescriptor.GetDefaultEvent()
    {
        return null;
    }

    /// <summary>
    /// Gets the Default Property
    /// </summary>
    PropertyDescriptor ICustomTypeDescriptor.GetDefaultProperty()
    {
        return null;
    }

    /// <summary>
    /// Gets the Editor
    /// </summary>
    object ICustomTypeDescriptor.GetEditor(Type editorBaseType)
    {
        return null;
    }

    /// <summary>
    /// Gets the Events
    /// </summary>
    EventDescriptorCollection ICustomTypeDescriptor.GetEvents(Attribute[] attributes)
    {
        return new EventDescriptorCollection(null);
    }

    /// <summary>
    /// Gets the events
    /// </summary>
    EventDescriptorCollection ICustomTypeDescriptor.GetEvents()
    {
        return new EventDescriptorCollection(null);
    }

    /// <summary>
    /// Gets the properties
    /// </summary>
    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties(Attribute[] attributes)
    {
        List<propertydescriptor> properties = new List<propertydescriptor>();

        //Add property descriptors for each entry in the dictionary
        foreach (string key in this.Keys)
        {
            properties.Add(new TestResultPropertyDescriptor(key));
        }

        //Get properties also belonging to this class also
        PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(this.GetType(), attributes);

        foreach (PropertyDescriptor oPropertyDescriptor in pdc)
        {
            properties.Add(oPropertyDescriptor);
        }

        return new PropertyDescriptorCollection(properties.ToArray());
    }

    /// <summary>
    /// gets the Properties
    /// </summary>
    PropertyDescriptorCollection ICustomTypeDescriptor.GetProperties()
    {
        return ((ICustomTypeDescriptor)this).GetProperties(null);
    }

    /// <summary>
    /// Gets the property owner
    /// </summary>
    object ICustomTypeDescriptor.GetPropertyOwner(PropertyDescriptor pd)
    {
        return this;
    }

    #endregion Methods

    //---------------------------------------------------------------------------------------------------------------------------
}

Remarque: dans la méthode GetProperties, je pourrais mettre en cache les PropertyDescriptors une fois lus pour les performances, mais comme j'ajoute et supprime des colonnes au moment de l'exécution, je veux toujours les reconstruire

Vous remarquerez également dans la méthode GetProperties que les descripteurs de propriété ajoutés pour les entrées de dictionnaire sont de type TestResultPropertyDescriptor. Il s'agit d'une classe de descripteur de propriété personnalisée qui gère la manière dont les propriétés sont définies et récupérées. Jetez un œil à l'implémentation ci-dessous:

/// <summary>
/// Property Descriptor for Test Result Row Wrapper
/// </summary>
public class TestResultPropertyDescriptor : PropertyDescriptor
{
    //- PROPERTIES --------------------------------------------------------------------------------------------------------------

    #region Properties

    /// <summary>
    /// Component Type
    /// </summary>
    public override Type ComponentType
    {
        get { return typeof(Dictionary<string, TestResultValue>); }
    }

    /// <summary>
    /// Gets whether its read only
    /// </summary>
    public override bool IsReadOnly
    {
        get { return false; }
    }

    /// <summary>
    /// Gets the Property Type
    /// </summary>
    public override Type PropertyType
    {
        get { return typeof(string); }
    }

    #endregion Properties

    //- CONSTRUCTOR -------------------------------------------------------------------------------------------------------------

    #region Constructor

    /// <summary>
    /// Constructor
    /// </summary>
    public TestResultPropertyDescriptor(string key)
        : base(key, null)
    {

    }

    #endregion Constructor

    //- METHODS -----------------------------------------------------------------------------------------------------------------

    #region Methods

    /// <summary>
    /// Can Reset Value
    /// </summary>
    public override bool CanResetValue(object component)
    {
        return true;
    }

    /// <summary>
    /// Gets the Value
    /// </summary>
    public override object GetValue(object component)
    {
          return ((Dictionary<string, TestResultValue>)component)[base.Name].Value;
    }

    /// <summary>
    /// Resets the Value
    /// </summary>
    public override void ResetValue(object component)
    {
        ((Dictionary<string, TestResultValue>)component)[base.Name].Value = string.Empty;
    }

    /// <summary>
    /// Sets the value
    /// </summary>
    public override void SetValue(object component, object value)
    {
        ((Dictionary<string, TestResultValue>)component)[base.Name].Value = value.ToString();
    }

    /// <summary>
    /// Gets whether the value should be serialized
    /// </summary>
    public override bool ShouldSerializeValue(object component)
    {
        return false;
    }

    #endregion Methods

    //---------------------------------------------------------------------------------------------------------------------------
}

Les principales propriétés à examiner sur cette classe sont GetValue et SetValue. Ici, vous pouvez voir le composant en cours de conversion en tant que dictionnaire et la valeur de la clé à l'intérieur en cours de définition ou de récupération. Il est important que le dictionnaire de cette classe soit du même type dans la classe wrapper Row, sinon la conversion échouera. Lorsque le descripteur est créé, la clé (nom de propriété) est transmise et est utilisée pour interroger le dictionnaire pour obtenir la valeur correcte.

Tiré de mon blog à:

Implémentation ICustomTypeDescriptor pour les propriétés dynamiques

WraithNath
la source
Je sais que vous avez écrit ceci il y a toujours, mais vous devriez vraiment mettre une partie de votre code dans votre réponse, ou citer quelque chose de votre message. Je pense que c'est dans les règles - votre réponse devient presque dénuée de sens si votre lien devenait sombre. Je ne vais pas voter contre parce que vous pouvez rechercher ICustomTypeDescriptor sur MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz
@DavidSchwartz - Ajouté.
WraithNath
J'ai exactement le même problème de conception que vous, cela semble être une bonne solution. Eh bien, soit cela, soit je supprime la liaison de données et contrôle manuellement l'interface utilisateur via le code derrière à mon avis. Pouvez-vous faire une liaison bidirectionnelle avec cette approche?
lance
@rolls oui vous pouvez, assurez-vous simplement que votre descripteur de propriété ne renvoie pas qu'il est en lecture seule. J'ai récemment utilisé une approche similaire pour quelque chose d'autre qui montre les données dans une liste d'arbres qui permet de modifier les données dans les cellules
WraithNath
1

Vous devez examiner les DependencyObjects tels qu'ils sont utilisés par WPF, ceux-ci suivent un modèle similaire dans lequel les propriétés peuvent être attribuées au moment de l'exécution. Comme mentionné ci-dessus, cela indique finalement l'utilisation d'une table de hachage.

Une autre chose utile à examiner est CSLA.Net . Le code est disponible gratuitement et utilise certains des principes \ modèles qu'il semble que vous recherchiez.

De plus, si vous envisagez de trier et de filtrer, je suppose que vous allez utiliser une sorte de grille. Une interface utile à implémenter est ICustomTypeDescriptor, cela vous permet de remplacer efficacement ce qui se passe lorsque votre objet est réfléchi afin que vous puissiez pointer le réflecteur vers la propre table de hachage interne de votre objet.

gsobocinski
la source
1

En remplacement d'une partie du code d'orsogufo, parce que j'ai récemment utilisé un dictionnaire pour ce même problème, voici mon opérateur []:

public string this[string key]
{
    get { return properties.ContainsKey(key) ? properties[key] : null; }

    set
    {
        if (properties.ContainsKey(key))
        {
            properties[key] = value;
        }
        else
        {
            properties.Add(key, value);
        }
    }
}

Avec cette implémentation, le setter ajoutera de nouvelles paires clé-valeur lorsque vous les utiliserez []=si elles n'existent pas déjà dans le dictionnaire.

De plus, pour moi, il propertiesy a un IDictionaryet dans les constructeurs sur lesquels je l'initialise new SortedDictionary<string, string>().

Sarah Vessels
la source
J'essaye votre solution. Je mets une valeur du côté du service comme record[name_column] = DBConvert.To<string>(r[name_column]);recordest mon DTO. Comment obtenir cette valeur côté client?
Rohaan
1

Je ne sais pas quelles sont vos raisons, et même si vous pouviez réussir d'une manière ou d'une autre avec Reflection Emit (je ne suis pas sûr que vous le puissiez), cela ne semble pas être une bonne idée. Ce qui est probablement une meilleure idée est d'avoir une sorte de dictionnaire et vous pouvez encapsuler l'accès au dictionnaire via des méthodes de votre classe. De cette façon, vous pouvez stocker les données de la base de données dans ce dictionnaire, puis les récupérer à l'aide de ces méthodes.

BGratuit
la source
0

Pourquoi ne pas utiliser un indexeur avec le nom de la propriété comme valeur de chaîne transmise à l'indexeur?

Randolpho
la source
0

Ne pourriez-vous pas simplement demander à votre classe d'exposer un objet Dictionary? Au lieu de «joindre plus de propriétés à l'objet», vous pouvez simplement insérer vos données (avec un identifiant) dans le dictionnaire au moment de l'exécution.

Rob
la source
0

Si c'est pour la liaison, vous pouvez référencer des indexeurs à partir de XAML

Text="{Binding [FullName]}"

Ici, il fait référence à l'indexeur de classe avec la clé "FullName"

Anish
la source