Jak utworzyć właściwości dynamiczne w C #?

87

Szukam sposobu na stworzenie klasy z zestawem właściwości statycznych. W czasie wykonywania chcę mieć możliwość dodawania innych właściwości dynamicznych do tego obiektu z bazy danych. Chciałbym również dodać możliwości sortowania i filtrowania do tych obiektów.

Jak to zrobić w C #?

Eatdoku
źródło
3
Jaki jest cel tych zajęć? Twoja prośba wzbudza we mnie podejrzenia, że ​​naprawdę potrzebujesz wzorca projektowego lub czegoś podobnego, chociaż brak wiedzy na temat twojego przypadku użycia oznacza, że ​​tak naprawdę nie mam sugestii.
Brian

Odpowiedzi:

60

Możesz na przykład użyć słownika

Dictionary<string,object> properties;

Myślę, że w większości przypadków, gdy robi się coś podobnego, robi się to w ten sposób.
W każdym razie nic nie zyskałbyś na tworzeniu "prawdziwej" właściwości z akcesoriami set i get, ponieważ byłaby ona tworzona tylko w czasie wykonywania i nie używałbyś jej w swoim kodzie ...

Oto przykład pokazujący możliwą implementację filtrowania i sortowania (bez sprawdzania błędów):

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
źródło
30

Jeśli to potrzebne do celów wiązania danych, można to zrobić z niestandardowym modelu deskryptora ... poprzez wdrożenie ICustomTypeDescriptor, TypeDescriptionProvideri / lub TypeCovertermożna tworzyć własne PropertyDescriptorinstancji w czasie wykonywania. To jest to, co podoba kontrole DataGridView, PropertyGridkorzystanie etc właściwościach ekranu.

Aby powiązać się z listami, potrzebujesz ITypedListi IList; do podstawowego sortowania: IBindingList; dla filtrowania i zaawansowanego sortowania: IBindingListView; dla pełnej obsługi "nowego wiersza" ( DataGridView): ICancelAddNew(uff!).

To jednak dużo pracy. DataTable(chociaż tego nienawidzę) to tani sposób na zrobienie tego samego. Jeśli nie potrzebujesz wiązania danych, po prostu użyj tablicy haszującej ;-p

Oto prosty przykład - ale możesz zrobić o wiele więcej ...

Marc Gravell
źródło
dzięki ... możliwość bezpośredniego wyszukiwania danych jest tym, czego szukałem. więc w zasadzie tanim sposobem jest przetłumaczenie kolekcji obiektów na DataTable, a następnie powiązanie tabeli. Myślę, że po konwersji jest więcej rzeczy, o które należy się martwić ... dzięki za wkład.
Eatdoku
Na marginesie, powiązanie danych przez ICustomTypeDescriptor nie jest obsługiwane przez Silverlight :(.
Curt Hagenlocher
Jako węzeł boczny na marginesie, Silverlight 5 wprowadził interfejs ICustomTypeProvider zamiast ICustomTypeDescriptor. ICustomTypeProvider został następnie przeniesiony do .NET Framework 4.5, aby umożliwić przenoszenie między Silverlight i .NET Framework. :).
Edward
12

Utwórz tabelę z haszowaniem o nazwie „Właściwości” i dodaj do niej swoje właściwości.

Aric TenEyck
źródło
12

Nie jestem pewien, czy naprawdę chcesz robić to, co mówisz, że chcesz , ale nie mam powodu, dlaczego!

Nie można dodawać właściwości do klasy po jej JIT.

Najbliższym możliwym rozwiązaniem byłoby dynamiczne utworzenie podtypu za pomocą funkcji Odbicie.Emituj i kopiuj istniejące pola, ale musisz samodzielnie zaktualizować wszystkie odniesienia do obiektu.

Nie byłbyś również w stanie uzyskać dostępu do tych właściwości w czasie kompilacji.

Coś jak:

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

Nie mam zainstalowanego VS na tym komputerze, więc daj mi znać, jeśli są jakieś poważne błędy (cóż ... poza ogromnymi problemami z wydajnością, ale nie napisałem specyfikacji!)

Teraz możesz go użyć:

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

Można go również używać jak normalnej właściwości w języku obsługującym późne wiązanie (na przykład VB.NET)

Alun Harford
źródło
4

Zrobiłem dokładnie to za pomocą interfejsu ICustomTypeDescriptor i słownika.

Implementowanie ICustomTypeDescriptor dla właściwości dynamicznych:

Niedawno miałem wymóg powiązania widoku siatki z obiektem rekordu, który może mieć dowolną liczbę właściwości, które można dodawać i usuwać w czasie wykonywania. Miało to umożliwić użytkownikowi dodanie nowej kolumny do zestawu wyników w celu wprowadzenia dodatkowego zestawu danych.

Można to osiągnąć, mając każdy „wiersz” danych jako słownik, w którym klucz jest nazwą właściwości, a wartością jest ciąg lub klasa, która może przechowywać wartość właściwości dla określonego wiersza. Oczywiście posiadanie listy obiektów słownika nie będzie mogło być przypisane do siatki. W tym miejscu pojawia się ICustomTypeDescriptor.

Tworząc klasę opakowania dla Dictionary i dostosowując ją do interfejsu ICustomTypeDescriptor, można przesłonić zachowanie zwracania właściwości obiektu.

Spójrz na implementację klasy danych „wiersz” poniżej:

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

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

Uwaga: w metodzie GetProperties mogę buforować pliki PropertyDescriptors po przeczytaniu w celu zwiększenia wydajności, ale ponieważ dodaje i usuwa kolumny w czasie wykonywania, zawsze chcę, aby były odbudowywane

W metodzie GetProperties zauważysz również, że Deskryptory właściwości dodane do wpisów słownika są typu TestResultPropertyDescriptor. Jest to niestandardowa klasa deskryptora właściwości, która zarządza ustawieniem i pobieraniem właściwości. Spójrz na implementację poniżej:

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

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

Główne właściwości tej klasy to GetValue i SetValue. Tutaj możesz zobaczyć rzutowany komponent jako słownik oraz ustawioną lub pobraną wartość klucza w nim zawartego. Ważne jest, aby słownik w tej klasie był tego samego typu w klasie otoki Row, w przeciwnym razie rzutowanie zakończy się niepowodzeniem. Podczas tworzenia deskryptora klucz (nazwa właściwości) jest przekazywany i używany do wysyłania zapytań do słownika w celu uzyskania poprawnej wartości.

Zrobione z mojego bloga pod adresem:

Implementacja ICustomTypeDescriptor dla właściwości dynamicznych

WraithNath
źródło
Wiem, że napisałeś to dawno temu, ale naprawdę powinieneś umieścić część swojego kodu w swojej odpowiedzi lub zacytować coś ze swojego posta. Myślę, że to jest w zasadach - twoja odpowiedź staje się prawie bez znaczenia, gdyby twój link zgasł. Nie zamierzam jednak głosować w dół, ponieważ możesz wyszukać ICustomTypeDescriptor w MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz
@DavidSchwartz - Dodano.
WraithNath
Mam dokładnie ten sam problem projektowy co Ty, to wygląda na dobre rozwiązanie. Cóż, albo to, albo zrezygnuję z wiązania danych i ręcznie steruję interfejsem użytkownika za pomocą kodu za moim zdaniem. Czy przy takim podejściu możesz wykonać dwustronne wiązanie?
rolki
@rolls tak, możesz tylko upewnić się, że deskryptor właściwości nie zwraca, że ​​jest tylko do odczytu. Niedawno zastosowałem podobne podejście do czegoś innego, co pokazuje dane w liście drzew, która umożliwia edycję danych w komórkach
WraithNath
1

Należy zajrzeć do DependencyObjects używanych przez WPF, które są zgodne z podobnym wzorcem, w którym właściwości można przypisać w czasie wykonywania. Jak wspomniano powyżej, ostatecznie wskazuje to na użycie tabeli skrótów.

Inną przydatną rzeczą, na którą warto zwrócić uwagę, jest CSLA.Net . Kod jest dostępny bezpłatnie i wykorzystuje niektóre z zasad \ wzorców, o których myślisz.

Również jeśli patrzysz na sortowanie i filtrowanie, zgaduję, że będziesz używać jakiejś siatki. Użytecznym interfejsem do zaimplementowania jest ICustomTypeDescriptor, który pozwala skutecznie zastąpić to, co dzieje się, gdy obiekt zostanie odzwierciedlony, dzięki czemu można skierować reflektor na własną wewnętrzną tabelę haszowania obiektu.

gsobociński
źródło
1

Jako zamiennik dla części kodu orsogufo, ponieważ ostatnio poszedłem ze słownikiem dla tego samego problemu, oto mój operator []:

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

W przypadku tej implementacji metoda ustawiająca doda nowe pary klucz-wartość, []=jeśli nie istnieją one jeszcze w słowniku.

Również dla mnie propertiesjest IDictionaryi w konstruktorach, w których go inicjalizuję new SortedDictionary<string, string>().

Sarah Vessels
źródło
Próbuję twojego rozwiązania. ustawiam wartości po stronie usług, record[name_column] = DBConvert.To<string>(r[name_column]);gdzie recordjest moje DTO. Jak uzyskać tę wartość po stronie klienta?
Rohaan
1

Nie jestem pewien, jakie są Twoje powody, a nawet jeśli mógłbyś to jakoś zrobić za pomocą Emitowania Refleksji (nie jestem pewien, czy możesz), to nie brzmi jak dobry pomysł. Prawdopodobnie lepszym pomysłem jest posiadanie jakiegoś rodzaju słownika i możesz zawijać dostęp do słownika za pomocą metod w swojej klasie. W ten sposób możesz przechowywać dane z bazy danych w tym słowniku, a następnie pobierać je przy użyciu tych metod.

BFree
źródło
0

Dlaczego nie użyć indeksatora z nazwą właściwości jako wartością ciągu przekazaną do indeksatora?

Randolpho
źródło
0

Czy nie moglibyście po prostu udostępnić swojej klasie obiektu Dictionary? Zamiast „dołączać więcej właściwości do obiektu”, możesz po prostu wstawić swoje dane (z jakimś identyfikatorem) do słownika w czasie wykonywania.

Obrabować
źródło
0

Jeśli jest przeznaczony do powiązania, możesz odwoływać się do indeksatorów z XAML

Text="{Binding [FullName]}"

Tutaj odwołuje się do indeksatora klas z kluczem „FullName”

Anish
źródło