¿Cómo creo propiedades dinámicas en C #?

87

Estoy buscando una forma de crear una clase con un conjunto de propiedades estáticas. En tiempo de ejecución, quiero poder agregar otras propiedades dinámicas a este objeto desde la base de datos. También me gustaría agregar capacidades de clasificación y filtrado a estos objetos.

¿Cómo hago esto en C #?

Eatdoku
fuente
3
¿Cuál es el propósito de esta clase? Su solicitud me hace sospechar que realmente necesita un patrón de diseño o algo así, aunque no saber cuál es su caso de uso significa que en realidad no tengo una sugerencia.
Brian

Respuestas:

60

Podría usar un diccionario, digamos

Dictionary<string,object> properties;

Creo que en la mayoría de los casos en los que se hace algo similar, se hace así.
En cualquier caso, no ganaría nada creando una propiedad "real" con los accesos set y get, ya que se crearía solo en tiempo de ejecución y no la usaría en su código ...

A continuación, se muestra un ejemplo que muestra una posible implementación de filtrado y clasificación (sin verificación de errores):

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
fuente
30

Si necesita esto para propósitos de enlace de datos, se puede hacer esto con un modelo de descriptor de costumbre ... mediante la implementación ICustomTypeDescriptor, TypeDescriptionProvidery / o TypeCoverter, puede crear sus propias PropertyDescriptorinstancias en tiempo de ejecución. Esto es lo que utilizan los controles como DataGridView, PropertyGridetc. para mostrar propiedades.

Para enlazar a listas, necesitaría ITypedListy IList; para la clasificación básica: IBindingList; para el filtrado y clasificación avanzada: IBindingListView; para soporte completo de "nueva fila" ( DataGridView): ICancelAddNew(¡uf!).

Sin embargo, es mucho trabajo. DataTable(aunque lo odio) es una forma barata de hacer lo mismo. Si no necesita el enlace de datos, simplemente use una tabla hash ;-p

Aquí hay un ejemplo simple , pero puede hacer mucho más ...

Marc Gravell
fuente
gracias ... poder enlazar datos directamente es lo que estaba buscando. entonces, básicamente, la forma económica de hacerlo es traducir la colección de objetos a DataTable y luego vincular la tabla. Supongo que también hay más cosas de las que preocuparse después de la conversión ... gracias por tu entrada
Eatdoku
Como nota al margen, Silverlight no admite el enlace de datos a través de ICustomTypeDescriptor :(.
Curt Hagenlocher
Como nodo lateral de la nota al margen, Silverlight 5 introdujo la interfaz ICustomTypeProvider en lugar de ICustomTypeDescriptor. ICustomTypeProvider fue posteriormente portado a .NET Framework 4.5, para permitir la portabilidad entre Silverlight y .NET Framework. :).
Edward
12

Crea una tabla hash llamada "Propiedades" y agrégale tus propiedades.

Aric TenEyck
fuente
12

No estoy seguro de que realmente quieras hacer lo que dices que quieres hacer , ¡pero no me corresponde a mí razonar por qué!

No puede agregar propiedades a una clase después de que se haya modificado.

Lo más cercano que podría conseguir sería crear dinámicamente un subtipo con Reflection.Emit y copie los campos existentes, pero tendría que actualizar todas las referencias al objeto usted mismo.

Tampoco podrá acceder a esas propiedades en tiempo de compilación.

Algo como:

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

No tengo VS instalado en esta máquina, así que avíseme si hay errores masivos (bueno ... además de los problemas de rendimiento masivos, ¡pero no escribí la especificación!)

Ahora puedes usarlo:

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

También puede usarlo como una propiedad normal en un lenguaje que admita enlaces tardíos (por ejemplo, VB.NET)

Alun Harford
fuente
4

He hecho exactamente esto con una interfaz ICustomTypeDescriptor y un diccionario.

Implementación de ICustomTypeDescriptor para propiedades dinámicas:

Recientemente tuve el requisito de vincular una vista de cuadrícula a un objeto de registro que podría tener cualquier cantidad de propiedades que se pueden agregar y eliminar en tiempo de ejecución. Esto fue para permitir que un usuario agregue una nueva columna a un conjunto de resultados para ingresar un conjunto adicional de datos.

Esto se puede lograr teniendo cada 'fila' de datos como un diccionario con la clave como el nombre de la propiedad y el valor como una cadena o una clase que puede almacenar el valor de la propiedad para la fila especificada. Por supuesto, tener una lista de objetos de diccionario no podrá vincularse a una cuadrícula. Aquí es donde entra ICustomTypeDescriptor.

Al crear una clase contenedora para el diccionario y hacer que se adhiera a la interfaz ICustomTypeDescriptor, se puede anular el comportamiento para devolver propiedades para un objeto.

Eche un vistazo a la implementación de la clase de 'fila' de datos a continuación:

/// <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: En el método GetProperties, pude almacenar en caché los PropertyDescriptors una vez leídos para el rendimiento, pero como estoy agregando y eliminando columnas en tiempo de ejecución, siempre quiero que se reconstruyan

También notará en el método GetProperties que los descriptores de propiedad agregados para las entradas del diccionario son del tipo TestResultPropertyDescriptor. Se trata de una clase de descriptor de propiedades personalizada que gestiona cómo se establecen y recuperan las propiedades. Eche un vistazo a la implementación a continuación:

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

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

Las principales propiedades a tener en cuenta en esta clase son GetValue y SetValue. Aquí puede ver el componente que se convierte como diccionario y el valor de la clave dentro de él se establece o se recupera. Es importante que el diccionario de esta clase sea del mismo tipo en la clase contenedora Row, de lo contrario, la conversión fallará. Cuando se crea el descriptor, se pasa la clave (nombre de propiedad) y se utiliza para consultar el diccionario para obtener el valor correcto.

Tomado de mi blog en:

Implementación de ICustomTypeDescriptor para propiedades dinámicas

WraithNath
fuente
Sé que escribiste esto hace una eternidad, pero realmente deberías poner parte de tu código en tu respuesta o citar algo de tu publicación. Creo que eso está en las reglas: su respuesta casi no tiene sentido si su enlace se apaga. Sin embargo, no voy a votar negativamente porque puede buscar ICustomTypeDescriptor en MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz
@DavidSchwartz - Agregado.
WraithNath
Tengo exactamente el mismo problema de diseño que tú, parece una buena solución. Bueno, esto o yo elimino el enlace de datos y controlo manualmente la interfaz de usuario a través del código detrás en mi opinión. ¿Puedes hacer un enlace bidireccional con este enfoque?
llega el
@rolls sí, solo asegúrese de que el descriptor de su propiedad no devuelva que es de solo lectura. Recientemente he usado un enfoque similar para otra cosa que también muestra los datos en una lista de árboles que permite editar los datos en las celdas
WraithNath
1

Debe buscar en DependencyObjects como los usa WPF, estos siguen un patrón similar por el cual las propiedades se pueden asignar en tiempo de ejecución. Como se mencionó anteriormente, esto finalmente apunta hacia el uso de una tabla hash.

Otra cosa útil para echar un vistazo es CSLA.Net . El código está disponible gratuitamente y usa algunos de los principios \ patrones que parece que buscas.

Además, si está buscando ordenar y filtrar, supongo que usará algún tipo de cuadrícula. Una interfaz útil para implementar es ICustomTypeDescriptor, esto le permite anular efectivamente lo que sucede cuando su objeto se refleja para que pueda apuntar el reflector a la propia tabla hash interna de su objeto.

gsobocinski
fuente
1

Como reemplazo de parte del código de orsogufo, debido a que recientemente usé un diccionario para este mismo problema, aquí está mi operador []:

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 esta implementación, el establecedor agregará nuevos pares clave-valor cuando los use []=si aún no existen en el diccionario.

Además, para mí propertieses un IDictionaryy en los constructores lo inicializo en new SortedDictionary<string, string>().

Sarah Vessels
fuente
Estoy probando tu solución. Estoy estableciendo valores en el lado del servicio como record[name_column] = DBConvert.To<string>(r[name_column]);dónde recordestá mi DTO. ¿Cómo obtengo este valor del lado del cliente?
Rohaan
1

No estoy seguro de cuáles son sus razones, e incluso si pudiera lograrlo de alguna manera con Reflection Emit (no estoy seguro de que pueda), no parece una buena idea. Lo que probablemente sea una mejor idea es tener algún tipo de diccionario y puede ajustar el acceso al diccionario a través de métodos en su clase. De esa manera, puede almacenar los datos de la base de datos en este diccionario y luego recuperarlos usando esos métodos.

BFree
fuente
0

¿Por qué no utilizar un indexador con el nombre de la propiedad como un valor de cadena que se pasa al indexador?

Randolpho
fuente
0

¿No podría simplemente hacer que su clase exponga un objeto Diccionario? En lugar de "adjuntar más propiedades al objeto", puede simplemente insertar sus datos (con algún identificador) en el diccionario en tiempo de ejecución.

Robar
fuente
0

Si es para enlace, puede hacer referencia a indexadores desde XAML

Text="{Binding [FullName]}"

Aquí se hace referencia al indexador de clases con la clave "FullName"

Anish
fuente