Wie erstelle ich dynamische Eigenschaften in C #?


88

Ich suche nach einer Möglichkeit, eine Klasse mit einer Reihe statischer Eigenschaften zu erstellen. Zur Laufzeit möchte ich diesem Objekt andere dynamische Eigenschaften aus der Datenbank hinzufügen können. Ich möchte diesen Objekten auch Sortier- und Filterfunktionen hinzufügen.

Wie mache ich das in C #?


3
Was ist der Zweck dieser Klasse? Ihre Anfrage macht mich misstrauisch, dass Sie wirklich ein Entwurfsmuster oder etwas anderes benötigen. Wenn Sie jedoch nicht wissen, was Ihr Anwendungsfall ist, habe ich eigentlich keinen Vorschlag.
Brian

Antworten:


60

Sie könnten beispielsweise ein Wörterbuch verwenden

Dictionary<string,object> properties;

Ich denke, in den meisten Fällen, in denen etwas Ähnliches getan wird, wird es so gemacht.
In jedem Fall würden Sie nichts davon haben, eine "echte" Eigenschaft mit set and get accessors zu erstellen, da diese nur zur Laufzeit erstellt würde und Sie sie nicht in Ihrem Code verwenden würden ...

Hier ist ein Beispiel, das eine mögliche Implementierung des Filterns und Sortierens zeigt (keine Fehlerprüfung):

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

Wenn Sie dies für Datenbindungszwecke benötigen, können Sie dies mit einem benutzerdefinierten Deskriptormodells ... durch die Implementierung ICustomTypeDescriptor, TypeDescriptionProviderund / oder TypeCoverterkönnen Sie Ihre eigenen erstellen PropertyDescriptorInstanzen zur Laufzeit. Dies ist , was Kontrollen mögen DataGridView, PropertyGridusw. Gebrauch Anzeigeeigenschaften.

Zum Binden an Listen benötigen Sie ITypedListund IList; für die Grundsortierung : IBindingList; zum Filtern und erweiterten Sortieren : IBindingListView; für volle "neue Reihe" Unterstützung ( DataGridView): ICancelAddNew(Puh!).

Es ist jedoch viel Arbeit. DataTable(obwohl ich es hasse) ist eine billige Art, dasselbe zu tun. Wenn Sie keine Datenbindung benötigen, verwenden Sie einfach eine Hashtabelle ;-p

Hier ist ein einfaches Beispiel - aber Sie können noch viel mehr tun ...


danke ... in der Lage zu sein, direkt Daten zu binden, ist das, wonach ich gesucht habe. Der billigste Weg, dies zu tun, besteht darin, die Objektsammlung in DataTable zu übersetzen und stattdessen die Tabelle zu binden. Ich denke, es gibt noch mehr Dinge, über die man sich nach der Konvertierung Sorgen machen muss. Vielen Dank für Ihre Eingabe.
Eatdoku

Nebenbei bemerkt, die Datenbindung über ICustomTypeDescriptor wird von Silverlight nicht unterstützt :(.
Curt Hagenlocher

Als Nebenknoten zur Randnotiz führte Silverlight 5 anstelle von ICustomTypeDescriptor die ICustomTypeProvider-Schnittstelle ein. ICustomTypeProvider wurde anschließend auf .NET Framework 4.5 portiert, um die Portabilität zwischen Silverlight und .NET Framework zu ermöglichen. :).
Edward


12

Erstellen Sie eine Hashtabelle mit dem Namen "Eigenschaften" und fügen Sie Ihre Eigenschaften hinzu.


12

Ich bin mir nicht sicher, ob du wirklich das tun willst, was du sagst , aber ich kann nicht begründen, warum!

Sie können einer Klasse keine Eigenschaften hinzufügen, nachdem sie JITed wurde.

Das Beste, was Sie bekommen könnten, wäre, dynamisch einen Untertyp mit Reflection zu erstellen. Lassen Sie die vorhandenen Felder los und kopieren Sie sie, aber Sie müssten alle Verweise auf das Objekt selbst aktualisieren.

Sie können zur Kompilierungszeit auch nicht auf diese Eigenschaften zugreifen.

Etwas wie:

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

Ich habe VS nicht auf diesem Computer installiert, also lassen Sie mich wissen, wenn es massive Fehler gibt (naja ... abgesehen von den massiven Leistungsproblemen, aber ich habe die Spezifikation nicht geschrieben!)

Jetzt können Sie es verwenden:

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

Sie können es auch wie eine normale Eigenschaft in einer Sprache verwenden, die eine späte Bindung unterstützt (z. B. VB.NET).


4

Ich habe genau dies mit einer ICustomTypeDescriptor-Schnittstelle und einem Wörterbuch getan.

Implementieren von ICustomTypeDescriptor für dynamische Eigenschaften:

Vor kurzem musste eine Rasteransicht an ein Datensatzobjekt gebunden werden, das eine beliebige Anzahl von Eigenschaften aufweisen kann, die zur Laufzeit hinzugefügt und entfernt werden können. Auf diese Weise konnte ein Benutzer einer Ergebnismenge eine neue Spalte hinzufügen, um einen zusätzlichen Datensatz einzugeben.

Dies kann erreicht werden, indem jede Datenzeile als Wörterbuch verwendet wird, wobei der Schlüssel der Eigenschaftsname und der Wert eine Zeichenfolge oder eine Klasse ist, in der der Wert der Eigenschaft für die angegebene Zeile gespeichert werden kann. Natürlich kann eine Liste von Wörterbuchobjekten nicht an ein Raster gebunden werden. Hier kommt der ICustomTypeDescriptor ins Spiel.

Durch Erstellen einer Wrapper-Klasse für das Dictionary und deren Einhaltung der ICustomTypeDescriptor-Schnittstelle kann das Verhalten für die Rückgabe von Eigenschaften für ein Objekt überschrieben werden.

Schauen Sie sich die Implementierung der Datenklassenzeile unten an:

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

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

Hinweis: In der GetProperties-Methode konnte ich die einmal gelesenen PropertyDescriptors für die Leistung zwischenspeichern, aber da ich zur Laufzeit Spalten hinzufüge und entferne, möchte ich immer, dass sie neu erstellt werden

Sie werden auch in der GetProperties-Methode feststellen, dass die für die Wörterbucheinträge hinzugefügten Eigenschaftsbeschreibungen vom Typ TestResultPropertyDescriptor sind. Dies ist eine benutzerdefinierte Property Descriptor-Klasse, die verwaltet, wie Eigenschaften festgelegt und abgerufen werden. Schauen Sie sich die Implementierung unten an:

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

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

Die wichtigsten Eigenschaften dieser Klasse sind GetValue und SetValue. Hier sehen Sie, wie die Komponente als Wörterbuch umgewandelt wird und der Wert des darin enthaltenen Schlüssels festgelegt oder abgerufen wird. Es ist wichtig, dass das Wörterbuch in dieser Klasse vom selben Typ in der Row-Wrapper-Klasse ist, da sonst die Umwandlung fehlschlägt. Beim Erstellen des Deskriptors wird der Schlüssel (Eigenschaftsname) übergeben und zum Abfragen des Wörterbuchs verwendet, um den richtigen Wert zu erhalten.

Entnommen aus meinem Blog unter:

ICustomTypeDescriptor Implementierung für dynamische Eigenschaften


Ich weiß, dass Sie dies für immer geschrieben haben, aber Sie sollten wirklich einen Teil Ihres Codes in Ihre Antwort einfügen oder etwas aus Ihrem Beitrag zitieren. Ich denke, das steht in den Regeln - Ihre Antwort wird fast bedeutungslos, wenn Ihr Link dunkel wird. Ich werde jedoch nicht abstimmen, da Sie ICustomTypeDescriptor auf MSDN ( msdn.microsoft.com/en-us/library/… ) nachschlagen können
David Schwartz

@ DavidSchwartz - Hinzugefügt.
WraithNath

Ich habe genau das gleiche Designproblem wie Sie, dies scheint eine gute Lösung zu sein. Nun, entweder dies oder ich verzichte auf die Datenbindung und steuere die Benutzeroberfläche aus meiner Sicht manuell über den Code dahinter. Können Sie mit diesem Ansatz eine wechselseitige Bindung durchführen?
rollt

@rolls Ja, das können Sie. Stellen Sie einfach sicher, dass Ihr Eigenschaftsdeskriptor nicht schreibgeschützt zurückgibt. Ich habe kürzlich einen ähnlichen Ansatz für etwas anderes verwendet, das die Daten in einer Baumliste anzeigt, mit der Daten in den Zellen bearbeitet werden können
WraithNath

1

Sie sollten sich DependencyObjects ansehen, wie sie von WPF verwendet werden. Diese folgen einem ähnlichen Muster, bei dem Eigenschaften zur Laufzeit zugewiesen werden können. Wie oben erwähnt, deutet dies letztendlich auf die Verwendung einer Hash-Tabelle hin.

Eine weitere nützliche Sache ist CSLA.Net . Der Code ist frei verfügbar und verwendet einige der Prinzipien \ Muster, nach denen Sie suchen.

Auch wenn Sie sich mit Sortieren und Filtern befassen, werden Sie vermutlich eine Art Raster verwenden. Eine nützliche Schnittstelle, die implementiert werden muss, ist ICustomTypeDescriptor. Auf diese Weise können Sie effektiv überschreiben, was passiert, wenn Ihr Objekt reflektiert wird, sodass Sie den Reflektor auf die interne Hash-Tabelle Ihres Objekts verweisen können.


1

Als Ersatz für einen Teil des Orsogufo-Codes, da ich kürzlich selbst ein Wörterbuch für dasselbe Problem erstellt habe, ist hier mein [] 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);
        }
    }
}

Bei dieser Implementierung fügt der Setter bei Verwendung neue Schlüssel-Wert-Paare hinzu, []=wenn diese nicht bereits im Wörterbuch vorhanden sind.

Auch für mich propertiesist ein IDictionaryund in Konstruktoren initialisiere ich es auf new SortedDictionary<string, string>().


Ich versuche Ihre Lösung. Ich gründe eine Werte an Serviceseite wie , record[name_column] = DBConvert.To<string>(r[name_column]);wo recordist meine DTO. Wie erhalte ich diesen Wert auf Kundenseite?
Rohaan

1

Ich bin mir nicht sicher, was Ihre Gründe sind, und selbst wenn Sie es mit Reflection Emit irgendwie schaffen könnten (ich bin mir nicht sicher, ob Sie das können), klingt es nicht nach einer guten Idee. Was wahrscheinlich eine bessere Idee ist, ist eine Art Wörterbuch, und Sie können den Zugriff auf das Wörterbuch über Methoden in Ihrer Klasse umbrechen. Auf diese Weise können Sie die Daten aus der Datenbank in diesem Wörterbuch speichern und sie dann mit diesen Methoden abrufen.


0

Warum nicht einen Indexer mit dem Eigenschaftsnamen als Zeichenfolgenwert verwenden, der an den Indexer übergeben wird?


0

Könnte Ihre Klasse nicht einfach ein Dictionary-Objekt verfügbar machen? Anstatt "mehr Eigenschaften an das Objekt anzuhängen", können Sie Ihre Daten zur Laufzeit einfach (mit einer Kennung) in das Wörterbuch einfügen.


0

Wenn es sich um eine Bindung handelt, können Sie auf Indexer aus XAML verweisen

Text="{Binding [FullName]}"

Hier wird auf den Klassenindexer mit dem Schlüssel "FullName" verwiesen.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.