Como faço para criar propriedades dinâmicas em C #?


88

Estou procurando uma maneira de criar uma classe com um conjunto de propriedades estáticas. Em tempo de execução, desejo poder adicionar outras propriedades dinâmicas a este objeto do banco de dados. Eu também gostaria de adicionar recursos de classificação e filtragem a esses objetos.

Como faço isso em C #?


3
Qual é o objetivo desta aula? Sua solicitação me faz suspeitar que você realmente precisa de um padrão de design ou algo assim, embora não saber qual é o seu caso de uso significa que eu realmente não tenho uma sugestão.
Brian

Respostas:


60

Você pode usar um dicionário, por exemplo

Dictionary<string,object> properties;

Acho que na maioria dos casos em que algo semelhante é feito, é feito assim.
Em qualquer caso, você não ganharia nada criando uma propriedade "real" com os acessos set e get, já que ela seria criada apenas em tempo de execução e você não a estaria usando em seu código ...

Aqui está um exemplo, mostrando uma possível implementação de filtragem e classificação (sem verificação de erros):

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 você precisa deste para fins de ligação de dados, você pode fazer isso com um modelo de descritor de costume ... através da implementação ICustomTypeDescriptor, TypeDescriptionProvidere / ou TypeCoverter, você pode criar suas próprias PropertyDescriptorinstâncias em tempo de execução. Isto é o que controla gosto DataGridView, PropertyGridetc uso de propriedades de exibição.

Para vincular a listas, você precisa ITypedListe IList; por classificação básica: IBindingList; para a filtragem e classificação avançada: IBindingListView; para suporte completo de "nova linha" ( DataGridView): ICancelAddNew(ufa!).

É muito trabalho, no entanto. DataTable(embora eu odeie) é uma maneira barata de fazer a mesma coisa. Se você não precisa de vinculação de dados, basta usar um hashtable ;-p

Aqui está um exemplo simples - mas você pode fazer muito mais ...


obrigado ... ser capaz de vincular dados diretamente é o que eu estava procurando. então, basicamente, a maneira mais barata de fazer isso é traduzir a coleção de objetos em DataTable e então vincular a tabela. Acho que há mais coisas com que se preocupar após a conversão também ... obrigado por sua contribuição.
Eatdoku

Como observação, a vinculação de dados por meio de ICustomTypeDescriptor não é compatível com Silverlight :(.
Curt Hagenlocher

Como um nó lateral à nota lateral, o Silverlight 5 introduziu a interface ICustomTypeProvider no lugar de ICustomTypeDescriptor. ICustomTypeProvider foi posteriormente portado para o .NET Framework 4.5, para permitir a portabilidade entre o Silverlight e o .NET Framework. :).
Edward


12

Crie um Hashtable chamado "Propriedades" e adicione suas propriedades a ele.


12

Não tenho certeza se você realmente deseja fazer o que diz que deseja fazer , mas não sou eu quem sabe o motivo!

Você não pode adicionar propriedades a uma classe após ela ter sido submetida ao JIT.

O mais próximo que você poderia obter seria criar dinamicamente um subtipo com Reflection.Emit e copiar os campos existentes, mas você mesmo teria que atualizar todas as referências ao objeto.

Você também não conseguiria acessar essas propriedades em tempo de compilação.

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

Eu não tenho o VS instalado nesta máquina, então deixe-me saber se houver algum bug enorme (bem ... além dos problemas de desempenho enormes, mas eu não escrevi a especificação!)

Agora você pode usá-lo:

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

Você também pode usá-lo como uma propriedade normal em uma linguagem que suporta vinculação tardia (por exemplo, VB.NET)


4

Fiz exatamente isso com uma interface ICustomTypeDescriptor e um Dicionário.

Implementando ICustomTypeDescriptor para propriedades dinâmicas:

Recentemente, tive um requisito para vincular uma exibição de grade a um objeto de registro que poderia ter qualquer número de propriedades que podem ser adicionadas e removidas em tempo de execução. Isso permitia que um usuário adicionasse uma nova coluna a um conjunto de resultados para inserir um conjunto adicional de dados.

Isso pode ser conseguido tendo cada 'linha' de dados como um dicionário com a chave sendo o nome da propriedade e o valor sendo uma string ou uma classe que pode armazenar o valor da propriedade para a linha especificada. É claro que ter uma Lista de objetos de dicionário não poderá ser vinculado a uma grade. É aqui que entra o ICustomTypeDescriptor.

Ao criar uma classe de invólucro para o Dicionário e fazê-la aderir à interface ICustomTypeDescriptor, o comportamento para retornar propriedades para um objeto pode ser substituído.

Dê uma olhada na implementação da classe de dados 'linha' abaixo:

/// <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: No método GetProperties, eu poderia armazenar em cache os PropertyDescriptors uma vez lidos para desempenho, mas como estou adicionando e removendo colunas em tempo de execução, eu sempre as quero reconstruídas

Você também notará no método GetProperties que os Descritores de Propriedade adicionados para as entradas do dicionário são do tipo TestResultPropertyDescriptor. Esta é uma classe de descritor de propriedade personalizada que gerencia como as propriedades são definidas e recuperadas. Dê uma olhada na implementação abaixo:

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

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

As propriedades principais a serem observadas nesta classe são GetValue e SetValue. Aqui você pode ver o componente sendo convertido como um dicionário e o valor da chave dentro dele sendo definido ou recuperado. É importante que o dicionário nesta classe seja do mesmo tipo na classe Row wrapper, caso contrário, a conversão falhará. Quando o descritor é criado, a chave (nome da propriedade) é passada e é usada para consultar o dicionário para obter o valor correto.

Retirado do meu blog em:

Implementação ICustomTypeDescriptor para propriedades dinâmicas


Eu sei que você escreveu isso há muito tempo, mas você realmente deveria colocar parte do código em sua resposta ou citar algo de sua postagem. Eu acho que está nas regras - sua resposta torna-se quase sem sentido se seu link ficar escuro. Não vou fazer downvote porque você pode pesquisar ICustomTypeDescriptor no MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz

@DavidSchwartz - Adicionado.
WraithNath

Tenho exatamente o mesmo problema de design que você, parece uma boa solução. Bem, ou isso ou eu acabo com a ligação de dados e controle manualmente a interface do usuário por meio do code-behind na minha visão. Você pode fazer vinculação bidirecional com essa abordagem?
rola em

@rolls sim, você pode, apenas certifique-se de que seu descritor de propriedade não retorne que é somente leitura. Eu usei uma abordagem semelhante recentemente para outra coisa também que mostra os dados em uma lista de árvores que permite que os dados sejam editados nas células
WraithNath

1

Você deve examinar DependencyObjects conforme usado pelo WPF; eles seguem um padrão semelhante em que as propriedades podem ser atribuídas no tempo de execução. Conforme mencionado acima, isso basicamente aponta para o uso de uma tabela hash.

Outra coisa útil para se dar uma olhada é CSLA.Net . O código está disponível gratuitamente e usa alguns dos princípios \ padrões que parece que você procura.

Além disso, se você estiver procurando por classificação e filtragem, estou supondo que usará algum tipo de grade. Uma interface útil para implementar é ICustomTypeDescriptor, que permite substituir efetivamente o que acontece quando seu objeto é refletido para que você possa apontar o refletor para a própria tabela hash interna do seu objeto.


1

Como uma substituição para parte do código de orsogufo, porque recentemente usei um dicionário para esse mesmo problema, aqui está meu 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);
        }
    }
}

Com esta implementação, o setter adicionará novos pares de valores-chave quando você os usar, []=se eles ainda não existirem no dicionário.

Além disso, para mim propertiesé um IDictionarye nos construtores que inicializo new SortedDictionary<string, string>().


Estou tentando sua solução. Estou definindo valores no lado do serviço, como record[name_column] = DBConvert.To<string>(r[name_column]);onde recordestá meu DTO. Como faço para obter esse valor no lado do cliente?
Rohaan

1

Não tenho certeza de quais são seus motivos, e mesmo que você pudesse conseguir isso de alguma forma com o Reflection Emit (não tenho certeza se você pode), não parece uma boa ideia. O que provavelmente é uma ideia melhor é ter algum tipo de Dicionário e você pode agrupar o acesso ao dicionário por meio de métodos em sua classe. Dessa forma, você pode armazenar os dados do banco de dados neste dicionário e, em seguida, recuperá-los usando esses métodos.


0

Por que não usar um indexador com o nome da propriedade como um valor de string passado ao indexador?


0

Você não poderia simplesmente fazer sua classe expor um objeto Dictionary? Em vez de "anexar mais propriedades ao objeto", você pode simplesmente inserir seus dados (com algum identificador) no dicionário em tempo de execução.


0

Se for para vinculação, você pode referenciar indexadores de XAML

Text="{Binding [FullName]}"

Aqui está referenciando o indexador de classe com a chave "FullName"

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.