Bagaimana cara membuat properti dinamis di C #?

87

Saya mencari cara untuk membuat kelas dengan satu set properti statis. Saat menjalankan, saya ingin menambahkan properti dinamis lainnya ke objek ini dari database. Saya juga ingin menambahkan kemampuan pengurutan dan pemfilteran ke objek ini.

Bagaimana saya melakukan ini di C #?

Eatdoku
sumber
3
Apa tujuan kelas ini? Permintaan Anda membuat saya curiga bahwa Anda benar-benar membutuhkan pola desain atau sesuatu, meskipun tidak mengetahui kasus penggunaan Anda berarti saya sebenarnya tidak punya saran.
Brian

Jawaban:

60

Anda mungkin menggunakan kamus, katakanlah

Dictionary<string,object> properties;

Saya pikir dalam banyak kasus di mana sesuatu yang serupa dilakukan, itu dilakukan seperti ini.
Bagaimanapun, Anda tidak akan mendapatkan apa pun dari membuat properti "nyata" dengan set dan get accessors, karena ini hanya akan dibuat pada waktu proses dan Anda tidak akan menggunakannya dalam kode Anda ...

Berikut adalah contoh, yang menunjukkan kemungkinan penerapan pemfilteran dan pengurutan (tanpa pemeriksaan kesalahan):

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
sumber
30

Jika Anda memerlukan ini untuk tujuan pengikatan data, Anda dapat melakukannya dengan model deskriptor khusus ... dengan mengimplementasikan ICustomTypeDescriptor, TypeDescriptionProviderdan / atau TypeCoverter, Anda dapat membuat PropertyDescriptorinstance Anda sendiri pada waktu proses. Seperti inilah kontrol DataGridView, PropertyGriddll. Gunakan untuk menampilkan properti.

Untuk mengikat ke daftar, Anda memerlukan ITypedListdan IList; untuk menyortir dasar: IBindingList; untuk penyaringan dan pemilahan canggih: IBindingListView; untuk dukungan penuh "baris baru" ( DataGridView): ICancelAddNew(Fiuh!).

Ini banyak pekerjaan. DataTable(meskipun saya benci) adalah cara murah untuk melakukan hal yang sama. Jika Anda tidak membutuhkan data-binding, cukup gunakan hashtable ;-p

Berikut adalah contoh sederhana - tetapi Anda dapat melakukan lebih banyak lagi ...

Marc Gravell
sumber
terima kasih ... dapat databind langsung adalah apa yang saya cari. jadi pada dasarnya cara murah untuk melakukannya adalah dengan menerjemahkan kumpulan objek ke dalam DataTable lalu mengikat tabel sebagai gantinya. Saya rasa ada lebih banyak hal yang perlu dikhawatirkan setelah konversi juga .. terima kasih atas masukan Anda.
Eatdoku
Sebagai catatan tambahan, pengikatan data melalui ICustomTypeDescriptor tidak didukung oleh Silverlight :(.
Curt Hagenlocher
Sebagai simpul samping ke catatan samping, Silverlight 5 memperkenalkan antarmuka ICustomTypeProvider sebagai pengganti ICustomTypeDescriptor. ICustomTypeProvider kemudian dipindahkan ke .NET Framework 4.5, untuk memungkinkan portabilitas antara Silverlight dan .NET Framework. :).
Edward
12

Buat Hashtable yang disebut "Properties" dan tambahkan properti Anda ke dalamnya.

Aric TenEyck
sumber
12

Saya tidak yakin Anda benar-benar ingin melakukan apa yang Anda katakan ingin Anda lakukan , tetapi bukan saya yang menjelaskan alasannya!

Anda tidak dapat menambahkan properti ke kelas setelah kelas tersebut di JIT.

Yang paling dekat yang bisa Anda dapatkan adalah membuat subtipe secara dinamis dengan Reflection. Hapus dan salin bidang yang ada, tetapi Anda harus memperbarui semua referensi ke objek itu sendiri.

Anda juga tidak akan dapat mengakses properti tersebut pada waktu kompilasi.

Sesuatu seperti:

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

Saya tidak memasang VS di mesin ini jadi beri tahu saya jika ada bug besar (yah ... selain masalah kinerja besar, tetapi saya tidak menulis spesifikasinya!)

Sekarang Anda dapat menggunakannya:

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

Anda juga dapat menggunakannya seperti properti normal dalam bahasa yang mendukung pengikatan terlambat (misalnya, VB.NET)

Alun Harford
sumber
4

Saya telah melakukan persis seperti ini dengan antarmuka ICustomTypeDescriptor dan Kamus.

Menerapkan ICustomTypeDescriptor untuk properti dinamis:

Saya baru-baru ini memiliki persyaratan untuk mengikat tampilan kisi ke objek rekaman yang dapat memiliki sejumlah properti yang dapat ditambahkan dan dihapus saat runtime. Ini untuk memungkinkan pengguna menambahkan kolom baru ke kumpulan hasil untuk memasukkan kumpulan data tambahan.

Hal ini dapat dicapai dengan memiliki setiap 'baris' data sebagai kamus dengan kunci menjadi nama properti dan nilainya menjadi string atau kelas yang dapat menyimpan nilai properti untuk baris yang ditentukan. Tentu saja memiliki objek List of Dictionary tidak akan bisa terikat ke kisi. Di sinilah ICustomTypeDescriptor masuk.

Dengan membuat kelas pembungkus untuk Kamus dan membuatnya sesuai dengan antarmuka ICustomTypeDescriptor, perilaku mengembalikan properti untuk suatu objek dapat diganti.

Lihatlah implementasi kelas 'baris' data di bawah ini:

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

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

Catatan: Dalam metode GetProperties I Could Cache the PropertyDescriptors sekali membaca untuk kinerja tetapi karena saya menambahkan dan menghapus kolom saat runtime, saya selalu ingin mereka dibangun kembali

Anda juga akan melihat dalam metode GetProperties bahwa Deskriptor Properti yang ditambahkan untuk entri kamus memiliki tipe TestResultPropertyDescriptor. Ini adalah kelas Descriptor Properti khusus yang mengatur bagaimana properti disetel dan diambil. Lihat implementasinya di bawah ini:

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

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

Properti utama untuk melihat kelas ini adalah GetValue dan SetValue. Di sini Anda dapat melihat komponen yang dicor sebagai kamus dan nilai kunci di dalamnya sedang Set atau diambil. Sangat penting bahwa kamus di kelas ini memiliki tipe yang sama di kelas pembungkus baris jika tidak cast akan gagal. Ketika deskriptor dibuat, kunci (nama properti) diteruskan dan digunakan untuk meminta kamus untuk mendapatkan nilai yang benar.

Diambil dari blog saya di:

Implementasi ICustomTypeDescriptor untuk properti dinamis

WraithNath
sumber
Saya tahu Anda menulis ini selamanya yang lalu, tetapi Anda benar-benar harus memasukkan beberapa kode Anda dalam jawaban Anda, atau mengutip sesuatu dari posting Anda. Saya pikir itu ada dalam aturan-- jawaban Anda menjadi hampir tidak berarti jika tautan Anda menjadi gelap. Tidak akan menurunkan suara karena Anda dapat mencari ICustomTypeDescriptor di MSDN ( msdn.microsoft.com/en-us/library/… )
David Schwartz
@DavidSchwartz - Ditambahkan.
WraithNath
Saya memiliki masalah desain yang persis sama dengan Anda, ini sepertinya solusi yang bagus. Baik ini atau saya menghilangkan penyatuan data dan secara manual mengontrol ui melalui kode di belakang dalam pandangan saya. Dapatkah Anda melakukan pengikatan dua arah dengan pendekatan ini?
gulungan
@ Gulungan ya Anda bisa, cukup pastikan deskriptor properti Anda tidak mengembalikan bahwa sifatnya hanya baca. Saya telah menggunakan pendekatan serupa baru-baru ini untuk sesuatu yang lain juga yang menunjukkan data dalam daftar hierarki yang memungkinkan data diedit dalam sel
WraithNath
1

Anda harus melihat DependencyObjects seperti yang digunakan oleh WPF, ini mengikuti pola yang sama di mana properti dapat ditetapkan saat runtime. Seperti disebutkan di atas, ini pada akhirnya mengarah ke penggunaan tabel hash.

Satu hal lain yang berguna untuk dilihat adalah CSLA.Net . Kode ini tersedia secara gratis dan menggunakan beberapa prinsip \ pola yang muncul setelah Anda.

Juga jika Anda melihat pengurutan dan pemfilteran, saya rasa Anda akan menggunakan semacam kisi. Antarmuka yang berguna untuk diterapkan adalah ICustomTypeDescriptor, ini memungkinkan Anda secara efektif menimpa apa yang terjadi ketika objek Anda direfleksikan sehingga Anda dapat mengarahkan reflektor ke tabel hash internal objek Anda sendiri.

gsobocinski.dll
sumber
1

Sebagai pengganti beberapa kode orsogufo, karena saya sendiri baru-baru ini menggunakan kamus untuk masalah yang sama ini, berikut operator [] saya:

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

Dengan implementasi ini, penyetel akan menambahkan pasangan nilai kunci baru saat Anda menggunakan []= jika pasangan tersebut belum ada dalam kamus.

Juga untuk saya properties adalah IDictionarydan dalam konstruktor saya menginisialisasi new SortedDictionary<string, string>().

Sarah Vessels
sumber
Saya mencoba solusi Anda. saya menetapkan nilai di sisi layanan seperti di record[name_column] = DBConvert.To<string>(r[name_column]);mana recordDTO saya. Bagaimana cara mendapatkan nilai ini di sisi klien?
RobertKing
1

Saya tidak yakin apa alasan Anda, dan bahkan jika Anda bisa melakukannya dengan Reflection Emit (saya 'tidak yakin Anda bisa), kedengarannya bukan ide yang bagus. Mungkin ide yang lebih baik adalah memiliki semacam Dictionary dan Anda dapat menggabungkan akses ke kamus melalui metode di kelas Anda. Dengan cara itu Anda bisa menyimpan data dari database dalam kamus ini, lalu mengambilnya menggunakan metode tersebut.

BFree
sumber
0

Mengapa tidak menggunakan pengindeks dengan nama properti sebagai nilai string yang diteruskan ke pengindeks?

Randolpho
sumber
0

Tidak bisakah Anda meminta kelas Anda mengekspos objek Dictionary? Alih-alih "melampirkan lebih banyak properti ke objek", Anda cukup memasukkan data Anda (dengan beberapa pengenal) ke dalam kamus saat menjalankan.

rampok
sumber
0

Jika itu untuk mengikat, maka Anda dapat mereferensikan pengindeks dari XAML

Text="{Binding [FullName]}"

Ini dia mereferensikan pengindeks kelas dengan kunci "FullName"

Anish
sumber