Как создать динамические свойства в C #?

87

Ищу способ создать класс с набором статических свойств. Во время выполнения я хочу иметь возможность добавлять к этому объекту другие динамические свойства из базы данных. Я также хотел бы добавить к этим объектам возможности сортировки и фильтрации.

Как мне это сделать на C #?

Eatdoku
источник
3
Какова цель этого занятия? Ваш запрос заставляет меня подозревать, что вам действительно нужен шаблон дизайна или что-то в этом роде, хотя незнание вашего варианта использования означает, что у меня на самом деле нет предложения.
Брайан

Ответы:

60

Вы можете использовать словарь, скажем

Dictionary<string,object> properties;

Я думаю, что в большинстве случаев, когда делается что-то подобное, это делается так.
В любом случае, вы ничего не получите от создания «реального» свойства с помощью аксессуаров set и get, поскольку оно будет создано только во время выполнения, и вы не будете использовать его в своем коде ...

Вот пример, показывающий возможную реализацию фильтрации и сортировки (без проверки ошибок):

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

Если вам это нужно для целей привязки данных, вы можете сделать это с помощью пользовательской модели дескриптора ... путем реализации ICustomTypeDescriptor, TypeDescriptionProviderи / или TypeCoverter, вы можете создавать свои собственные PropertyDescriptorэкземпляры во время выполнения. Это то , что контроль нравится DataGridView, и PropertyGridт.д. использовать для отображения свойств.

Для привязки к спискам вам потребуются ITypedListи IList; для базовой сортировки: IBindingList; для фильтрации и передовой сортировки: IBindingListView; для полной поддержки "новой строки" ( DataGridView): ICancelAddNew(уф!).

Хотя это большая работа. DataTable(хотя я ненавижу это) - дешевый способ сделать то же самое. Если вам не нужна привязка данных, просто используйте хеш-таблицу ;-p

Вот простой пример - но вы можете сделать гораздо больше ...

Марк Гравелл
источник
спасибо ... возможность привязки данных напрямую - это то, что я искал. поэтому в основном дешевый способ сделать это - перевести коллекцию объектов в DataTable, а затем вместо этого привязать таблицу. Я думаю, есть еще кое-что, о чем нужно беспокоиться после преобразования ... Спасибо за ваш вклад.
Eatdoku 03
В качестве примечания: привязка данных через ICustomTypeDescriptor не поддерживается Silverlight :(.
Курт Хагенлохер,
В качестве побочного элемента к боковой заметке Silverlight 5 представил интерфейс ICustomTypeProvider вместо ICustomTypeDescriptor. ICustomTypeProvider впоследствии был перенесен на .NET Framework 4.5, чтобы обеспечить переносимость между Silverlight и .NET Framework. :).
Эдвард
12

Создайте хеш-таблицу под названием «Свойства» и добавьте к ней свои свойства.

Арик Тенейк
источник
12

Я не уверен, что вы действительно хотите делать то, что говорите , но не мне объяснять, почему!

Вы не можете добавлять свойства к классу после того, как он был JITed.

Самое близкое, что вы могли бы сделать, - это динамически создать подтип с Reflection.Emit и скопировать существующие поля, но вам придется обновить все ссылки на объект самостоятельно.

Вы также не сможете получить доступ к этим свойствам во время компиляции.

Что-то типа:

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

У меня нет VS, установленного на этой машине, поэтому дайте мне знать, если есть какие-то серьезные ошибки (ну ... кроме серьезных проблем с производительностью, но я не писал спецификацию!)

Теперь вы можете использовать это:

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

Вы также можете использовать его как обычное свойство на языке, поддерживающем позднее связывание (например, VB.NET).

Алан Харфорд
источник
4

Я сделал именно это с помощью интерфейса ICustomTypeDescriptor и словаря.

Реализация ICustomTypeDescriptor для динамических свойств:

Недавно у меня появилось требование привязать представление сетки к объекту записи, который может иметь любое количество свойств, которые можно добавлять и удалять во время выполнения. Это должно было позволить пользователю добавить новый столбец в набор результатов для ввода дополнительного набора данных.

Этого можно достичь, используя каждую «строку» данных в качестве словаря, где ключ является именем свойства, а значение является строкой или классом, который может хранить значение свойства для указанной строки. Конечно, объекты List of Dictionary не могут быть привязаны к сетке. Здесь на помощь приходит ICustomTypeDescriptor.

Создав класс-оболочку для словаря и сделав его привязанным к интерфейсу ICustomTypeDescriptor, можно изменить поведение при возврате свойств для объекта.

Взгляните на реализацию класса строки данных ниже:

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

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

Примечание: в методе GetProperties я мог кэшировать дескрипторы PropertyDescriptors после чтения для повышения производительности, но поскольку я добавляю и удаляю столбцы во время выполнения, я всегда хочу, чтобы они перестраивались.

Вы также заметите в методе GetProperties, что дескрипторы свойств, добавленные для словарных статей, имеют тип TestResultPropertyDescriptor. Это настраиваемый класс дескриптора свойства, который управляет тем, как свойства устанавливаются и извлекаются. Взгляните на реализацию ниже:

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

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

Основные свойства этого класса - GetValue и SetValue. Здесь вы можете увидеть, что компонент приводится как словарь, а значение ключа внутри него устанавливается или извлекается. Важно, чтобы словарь в этом классе был того же типа, что и в классе-оболочке Row, в противном случае приведение не будет выполнено. При создании дескриптора передается ключ (имя свойства), который используется для запроса словаря для получения правильного значения.

Взято из моего блога по адресу:

ICustomTypeDescriptor Реализация для динамических свойств

WraithNath
источник
Я знаю, что вы написали это вечно назад, но вам действительно стоит вставить часть своего кода в свой ответ или процитировать что-нибудь из своего сообщения. Я думаю, что это в правилах - ваш ответ станет почти бессмысленным, если ваша ссылка отключится. Я не собираюсь голосовать против, потому что вы можете найти ICustomTypeDescriptor на MSDN ( msdn.microsoft.com/en-us/library/… )
Дэвид Шварц,
@DavidSchwartz - Добавлено.
WraithNath
У меня точно такая же дизайнерская проблема, как и у вас, похоже, это хорошее решение. Либо это, либо я откажусь от привязки данных и, на мой взгляд, вручную управляю пользовательским интерфейсом через код. Можете ли вы сделать двустороннюю привязку с помощью этого подхода?
rolls
@rolls да, вы можете, просто убедитесь, что ваш дескриптор свойства не возвращает его только для чтения. Недавно я использовал аналогичный подход для чего-то еще, что показывает данные в древовидном списке, который позволяет редактировать данные в ячейках
WraithNath
1

Вам следует изучить DependencyObjects, используемые WPF, они следуют аналогичному шаблону, в соответствии с которым свойства могут быть назначены во время выполнения. Как упоминалось выше, это в конечном итоге указывает на использование хеш-таблицы.

Еще одна полезная вещь, на которую стоит обратить внимание, - это CSLA.Net . Код находится в свободном доступе и использует некоторые принципы \ шаблоны, которые, похоже, вам нужны.

Также, если вы смотрите на сортировку и фильтрацию, я предполагаю, что вы собираетесь использовать какую-то сетку. Полезным интерфейсом для реализации является ICustomTypeDescriptor, который позволяет вам эффективно переопределить то, что происходит, когда ваш объект отражается, чтобы вы могли указать отражателем на собственную внутреннюю хеш-таблицу вашего объекта.

Gsobocinski
источник
1

В качестве замены некоторого кода orsogufo, поскольку я недавно сам использовал словарь для этой же проблемы, вот мой оператор []:

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

В этой реализации установщик будет добавлять новые пары ключ-значение, когда вы используете []= если они еще не существуют в словаре.

Кроме того, для меня propertiesэто IDictionaryи в конструкторах, которыми я его инициализирую new SortedDictionary<string, string>().

Сара Сосуды
источник
Пробую ваше решение. Я устанавливаю значения на стороне обслуживания, record[name_column] = DBConvert.To<string>(r[name_column]);где recordнаходится мой DTO. Как мне получить это значение на стороне клиента?
Rohaan
1

Я не уверен, каковы ваши причины, и даже если бы вы могли как-то справиться с этим с помощью Reflection Emit (я не уверен, что вы сможете), это не похоже на хорошую идею. Вероятно, лучшая идея - это иметь какой-то словарь, и вы можете обернуть доступ к словарю через методы своего класса. Таким образом, вы можете хранить данные из базы данных в этом словаре, а затем извлекать их с помощью этих методов.

BFree
источник
0

Почему бы не использовать индексатор с именем свойства в качестве строкового значения, передаваемого в индексатор?

Randolpho
источник
0

Не могли бы вы просто предоставить вашему классу объект Dictionary? Вместо «добавления дополнительных свойств к объекту» вы можете просто вставить свои данные (с некоторым идентификатором) в словарь во время выполнения.

Роб
источник
0

Если это для привязки, вы можете ссылаться на индексаторы из XAML

Text="{Binding [FullName]}"

Здесь он ссылается на индексатор класса с ключом "FullName"

Аниш
источник