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