Come si creano proprietà dinamiche in C #?


87

Sto cercando un modo per creare una classe con un insieme di proprietà statiche. In fase di esecuzione, voglio essere in grado di aggiungere altre proprietà dinamiche a questo oggetto dal database. Vorrei anche aggiungere funzionalità di ordinamento e filtro a questi oggetti.

Come si esegue questa operazione in C #?


3
Qual è lo scopo di questa lezione? La tua richiesta mi fa sospettare che tu abbia davvero bisogno di un modello di progettazione o qualcosa del genere, anche se non sapere quale sia il tuo caso d'uso significa che in realtà non ho un suggerimento.
Brian il

Risposte:


60

Potresti usare un dizionario, diciamo

Dictionary<string,object> properties;

Penso che nella maggior parte dei casi in cui viene fatto qualcosa di simile, sia fatto in questo modo.
In ogni caso, non guadagneresti nulla dalla creazione di una proprietà "reale" con set and get accessors, poiché verrebbe creata solo in fase di esecuzione e non la useresti nel tuo codice ...

Ecco un esempio, che mostra una possibile implementazione del filtro e dell'ordinamento (nessun controllo degli errori):

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

    }
}

30

Se avete bisogno di questo per scopi di associazione dati, è possibile farlo con un modello di descrittore personalizzato ... implementando ICustomTypeDescriptor, TypeDescriptionProvidere / o TypeCoverter, è possibile creare le proprie PropertyDescriptoristanze in fase di esecuzione. Questo è ciò che i controlli come DataGridView, PropertyGridecc. Usano per visualizzare le proprietà.

Per legare a elenchi, avresti bisogno di ITypedListe IList; per l'ordinamento di base: IBindingList; per il filtraggio e l'ordinamento avanzato: IBindingListView; per il pieno supporto "nuova riga" ( DataGridView): ICancelAddNew(phew!).

Tuttavia, è molto lavoro. DataTable(anche se lo odio) è un modo economico per fare la stessa cosa. Se non hai bisogno del data-binding, usa semplicemente una tabella hash ;-p

Ecco un semplice esempio , ma puoi fare molto di più ...


grazie ... essere in grado di eseguire il databind direttamente è quello che stavo cercando. quindi fondamentalmente il modo economico per farlo è tradurre la raccolta di oggetti in DataTable, quindi associare la tabella. Immagino che ci siano più cose di cui preoccuparsi anche dopo la conversione .. grazie per il tuo contributo.
Eatdoku

Come nota a margine, l'associazione dei dati tramite ICustomTypeDescriptor non è supportata da Silverlight :(.
Curt Hagenlocher

Come nodo laterale della nota a margine, Silverlight 5 ha introdotto l'interfaccia ICustomTypeProvider al posto di ICustomTypeDescriptor. ICustomTypeProvider è stato successivamente portato su .NET Framework 4.5, per consentire la portabilità tra Silverlight e .NET Framework. :).
Edward


12

Crea una tabella hash chiamata "Proprietà" e aggiungici le tue proprietà.


12

Non sono sicuro che tu voglia davvero fare quello che dici di voler fare , ma non spetta a me ragionare sul perché!

Non è possibile aggiungere proprietà a una classe dopo che è stata JIT.

La cosa più vicina che potresti ottenere sarebbe creare dinamicamente un sottotipo con Reflection. Emetti e copia i campi esistenti, ma dovresti aggiornare tu stesso tutti i riferimenti all'oggetto.

Inoltre non saresti in grado di accedere a queste proprietà in fase di compilazione.

Qualcosa di simile a:

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

Non ho VS installato su questa macchina, quindi fammi sapere se ci sono bug enormi (beh ... a parte gli enormi problemi di prestazioni, ma non ho scritto le specifiche!)

Ora puoi usarlo:

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

Puoi anche usarlo come una normale proprietà in un linguaggio che supporta l'associazione tardiva (ad esempio, VB.NET)


4

Ho fatto esattamente questo con un'interfaccia ICustomTypeDescriptor e un dizionario.

Implementazione di ICustomTypeDescriptor per proprietà dinamiche:

Recentemente ho avuto la necessità di associare una visualizzazione griglia a un oggetto record che potrebbe avere un numero qualsiasi di proprietà che possono essere aggiunte e rimosse in fase di esecuzione. Questo per consentire a un utente di aggiungere una nuova colonna a un set di risultati per inserire un ulteriore set di dati.

Ciò può essere ottenuto utilizzando ciascuna "riga" di dati come dizionario con la chiave come nome della proprietà e il valore come una stringa o una classe in grado di memorizzare il valore della proprietà per la riga specificata. Ovviamente, avere un elenco di oggetti Dizionario non sarà in grado di essere associato a una griglia. È qui che entra in gioco ICustomTypeDescriptor.

Creando una classe wrapper per il Dictionary e facendolo aderire all'interfaccia ICustomTypeDescriptor, è possibile sovrascrivere il comportamento per la restituzione delle proprietà per un oggetto.

Dai un'occhiata all'implementazione della classe data 'row' di seguito:

/// <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

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

Nota: nel metodo GetProperties potrei memorizzare nella cache i PropertyDescriptors una volta letti per le prestazioni, ma poiché sto aggiungendo e rimuovendo colonne in fase di esecuzione, voglio sempre che vengano ricostruiti

Noterai anche nel metodo GetProperties che i descrittori di proprietà aggiunti per le voci del dizionario sono di tipo TestResultPropertyDescriptor. Si tratta di una classe descrittore di proprietà personalizzata che gestisce il modo in cui le proprietà vengono impostate e recuperate. Dai un'occhiata all'implementazione di seguito:

/// <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

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

Le proprietà principali da esaminare in questa classe sono GetValue e SetValue. Qui puoi vedere il componente di cui viene eseguito il cast come dizionario e il valore della chiave al suo interno che viene impostato o recuperato. È importante che il dizionario in questa classe sia dello stesso tipo nella classe wrapper Row, altrimenti il ​​cast fallirà. Quando viene creato il descrittore, la chiave (nome della proprietà) viene passata e viene utilizzata per interrogare il dizionario per ottenere il valore corretto.

Tratto dal mio blog all'indirizzo:

ICustomTypeDescriptor Implementazione per proprietà dinamiche


So che l'hai scritto da sempre, ma dovresti davvero inserire un po 'del tuo codice nella tua risposta, o citare qualcosa dal tuo post. Penso che sia nelle regole - la tua risposta diventa quasi priva di significato se il tuo collegamento dovesse diventare oscuro. Non si vota in basso perché è possibile cercare ICustomTypeDescriptor su MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz

@DavidSchwartz - Aggiunto.
WraithNath

Ho esattamente lo stesso problema di progettazione di te, questa sembra una buona soluzione. Bene, o questo o elimino l'associazione dati e controllo manualmente l'interfaccia utente tramite il codice dietro a mio avviso. Riesci a legare a due vie con questo approccio?
rotola il

@rolls si puoi, assicurati solo che il descrittore della tua proprietà non restituisca che è di sola lettura. Recentemente ho utilizzato un approccio simile anche per qualcos'altro che mostra i dati in un elenco ad albero che consente di modificare i dati nelle celle
WraithNath

1

Dovresti esaminare DependencyObjects usati da WPF, questi seguono uno schema simile in base al quale le proprietà possono essere assegnate in fase di esecuzione. Come accennato in precedenza, alla fine questo punta verso l'utilizzo di una tabella hash.

Un'altra cosa utile a cui dare un'occhiata è CSLA.Net . Il codice è disponibile gratuitamente e utilizza alcuni dei principi \ modelli che sembra che tu stia cercando.

Inoltre, se stai cercando di ordinare e filtrare, immagino che utilizzerai una sorta di griglia. Un'interfaccia utile da implementare è ICustomTypeDescriptor, che ti consente di sovrascrivere efficacemente ciò che accade quando il tuo oggetto viene riflesso in modo da poter puntare il riflettore alla tabella hash interna del tuo oggetto.


1

In sostituzione di parte del codice di orsogufo, perché recentemente ho scelto un dizionario per lo stesso problema, ecco il mio operatore []:

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

Con questa implementazione, il setter aggiungerà nuove coppie chiave-valore quando le utilizzi, []=se non esistono già nel dizionario.

Inoltre, per me propertiesè un IDictionarye nei costruttori lo inizializzo new SortedDictionary<string, string>().


Sto provando la tua soluzione. sto impostando un valore sul lato del servizio come record[name_column] = DBConvert.To<string>(r[name_column]);dov'è il recordmio DTO. Come ottengo questo valore dal lato client?
Rohaan

1

Non sono sicuro di quali siano le tue ragioni, e anche se potresti farcela in qualche modo con Reflection Emit (non sono sicuro che tu possa farlo), non suona come una buona idea. Probabilmente un'idea migliore è avere una sorta di dizionario e puoi accedere al dizionario attraverso i metodi della tua classe. In questo modo è possibile memorizzare i dati dal database in questo dizionario e quindi recuperarli utilizzando questi metodi.


0

Perché non utilizzare un indicizzatore con il nome della proprietà come valore di stringa passato all'indicizzatore?


0

Non potresti semplicemente far esporre alla tua classe un oggetto Dictionary? Invece di "allegare più proprietà all'oggetto", potresti semplicemente inserire i tuoi dati (con qualche identificatore) nel dizionario in fase di esecuzione.


0

Se è per l'associazione, puoi fare riferimento agli indicizzatori da XAML

Text="{Binding [FullName]}"

Qui fa riferimento all'indicizzatore di classe con la chiave "FullName"

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.