Làm cách nào để tạo thuộc tính động trong C #?


87

Tôi đang tìm cách tạo một lớp với một tập hợp các thuộc tính tĩnh. Tại thời điểm chạy, tôi muốn có thể thêm các thuộc tính động khác vào đối tượng này từ cơ sở dữ liệu. Tôi cũng muốn thêm khả năng sắp xếp và lọc cho các đối tượng này.

Làm cách nào để thực hiện việc này trong C #?


3
Mục đích của lớp học này là gì? Yêu cầu của bạn khiến tôi nghi ngờ rằng bạn thực sự cần một mẫu thiết kế hoặc thứ gì đó, mặc dù không biết trường hợp sử dụng của bạn là gì có nghĩa là tôi thực sự không có đề xuất.
— Brian

Câu trả lời:


59

Bạn có thể sử dụng từ điển, nói

Dictionary<string,object> properties;

Tôi nghĩ rằng trong hầu hết các trường hợp, một cái gì đó tương tự được thực hiện, nó sẽ được thực hiện như thế này.
Trong mọi trường hợp, bạn sẽ không thu được gì từ việc tạo thuộc tính "thực" với bộ truy cập set và get, vì nó sẽ chỉ được tạo khi chạy và bạn sẽ không sử dụng nó trong mã của mình ...

Đây là một ví dụ, cho thấy khả năng triển khai lọc và sắp xếp (không kiểm tra lỗi):

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

Nếu bạn cần điều này cho các mục đích liên kết dữ liệu, bạn có thể làm điều này với một mô hình mô tả tùy chỉnh ... bằng cách thực hiện ICustomTypeDescriptor, TypeDescriptionProvidervà / hoặc TypeCoverterbạn có thể tạo riêng của bạn PropertyDescriptorví dụ khi chạy. Đây là những gì các điều khiển như DataGridView, PropertyGridv.v. sử dụng để hiển thị các thuộc tính.

Để liên kết với danh sách, bạn cần ITypedListvà IList; để phân loại cơ bản : IBindingList; để lọc và sắp xếp nâng cao : IBindingListView; để hỗ trợ đầy đủ "hàng mới" ( DataGridView): ICancelAddNew(phew!).

Đó là rất nhiều công việc mặc dù. DataTable(mặc dù tôi ghét nó) là một cách rẻ tiền để làm điều tương tự. Nếu bạn không cần ràng buộc dữ liệu, chỉ cần sử dụng bảng băm ;-p

Đây là một ví dụ đơn giản - nhưng bạn có thể làm được nhiều hơn thế ...


cảm ơn ... có thể databind trực tiếp là những gì tôi đang tìm kiếm. vì vậy về cơ bản, cách thực hiện rẻ là dịch tập hợp đối tượng thành DataTable sau đó ràng buộc bảng. Tôi đoán cũng có nhiều điều phải lo lắng sau khi chuyển đổi .. cảm ơn bạn đã đóng góp ý kiến.
— Eatdoku

Là một mặt lưu ý, dữ liệu ràng buộc thông qua ICustomTypeDescriptor không được hỗ trợ bởi Silverlight :(.
— Curt Hagenlocher

Là một nút phụ của ghi chú bên, Silverlight 5 đã giới thiệu giao diện ICustomTypeProvider thay cho ICustomTypeDescriptor. ICustomTypeProvider sau đó đã được chuyển sang .NET Framework 4.5, để cho phép khả năng di chuyển giữa Silverlight và .NET Framework. :).
— Edward


12

Tạo một Hashtable có tên "Thuộc tính" và thêm thuộc tính của bạn vào đó.


12

Tôi không chắc bạn thực sự muốn làm những gì bạn nói rằng bạn muốn làm , nhưng tôi không biết lý do tại sao!

Bạn không thể thêm thuộc tính vào một lớp sau khi nó đã được JITed.

Cách gần nhất mà bạn có thể thực hiện là tạo động một kiểu con với Reflection. Gửi và sao chép các trường hiện có, nhưng bạn phải tự cập nhật tất cả các tham chiếu đến đối tượng.

Bạn cũng sẽ không thể truy cập các thuộc tính đó tại thời điểm biên dịch.

Cái gì đó như:

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

Tôi chưa cài đặt VS trên máy này vì vậy hãy cho tôi biết nếu có bất kỳ lỗi lớn nào (tốt ... ngoài các vấn đề về hiệu suất lớn, nhưng tôi đã không viết thông số kỹ thuật!)

Bây giờ bạn có thể sử dụng nó:

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

Bạn cũng có thể sử dụng nó như một thuộc tính bình thường trong một ngôn ngữ hỗ trợ tính năng liên kết trễ (ví dụ: VB.NET)


4

Tôi đã thực hiện chính xác điều này với giao diện ICustomTypeDescriptor và Từ điển.

Triển khai ICustomTypeDescriptor cho các thuộc tính động:

Gần đây tôi đã có yêu cầu ràng buộc một chế độ xem lưới với một đối tượng bản ghi có thể có bất kỳ số lượng thuộc tính nào có thể được thêm vào và loại bỏ trong thời gian chạy. Điều này là để cho phép người dùng thêm một cột mới vào tập kết quả để nhập một tập dữ liệu bổ sung.

Điều này có thể đạt được bằng cách đặt mỗi 'hàng' dữ liệu làm từ điển với khóa là tên thuộc tính và giá trị là một chuỗi hoặc một lớp có thể lưu trữ giá trị của thuộc tính cho hàng được chỉ định. Tất nhiên có một Danh sách các đối tượng Từ điển sẽ không thể bị ràng buộc vào một lưới. Đây là nơi ICustomTypeDescriptor xuất hiện.

Bằng cách tạo một lớp bao bọc cho Từ điển và làm cho nó tuân thủ giao diện ICustomTypeDescriptor, hành vi trả về các thuộc tính cho một đối tượng có thể bị ghi đè.

Hãy xem việc triển khai lớp 'hàng' dữ liệu bên dưới:

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

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

Lưu ý: Trong phương thức GetProperties, tôi có thể lưu vào bộ nhớ cache các PropertyDescriptors đã đọc để có hiệu suất nhưng khi thêm và xóa các cột trong thời gian chạy, tôi luôn muốn chúng được xây dựng lại

Bạn cũng sẽ nhận thấy trong phương thức GetProperties rằng Bộ mô tả thuộc tính được thêm vào cho các mục từ điển thuộc loại TestResultPropertyDescriptor. Đây là một lớp Bộ mô tả thuộc tính tùy chỉnh quản lý cách các thuộc tính được thiết lập và truy xuất. Hãy xem cách triển khai bên dưới:

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

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

Các thuộc tính chính cần xem xét trên lớp này là GetValue và SetValue. Tại đây, bạn có thể thấy thành phần đang được nhập dưới dạng từ điển và giá trị của khóa bên trong nó được Đặt hoặc được truy xuất. Điều quan trọng là từ điển trong lớp này phải cùng loại trong lớp Trình bao bọc hàng nếu không quá trình ép kiểu sẽ không thành công. Khi bộ mô tả được tạo, khóa (tên thuộc tính) được chuyển vào và được sử dụng để truy vấn từ điển để lấy giá trị chính xác.

Lấy từ blog của tôi tại:

Thực thi ICustomTypeDescriptor cho các thuộc tính động


Tôi biết bạn đã viết điều này mãi mãi trước đây, nhưng bạn thực sự nên đặt một số mã của bạn trong câu trả lời của bạn hoặc trích dẫn điều gì đó từ bài đăng của bạn. Tôi nghĩ đó là quy tắc-- câu trả lời của bạn gần như trở nên vô nghĩa nếu liên kết của bạn bị tối. Tuy nhiên, sẽ không downvote vì bạn có thể tra cứu ICustomTypeDescriptor trên MSDN ( msdn.microsoft.com/en-us/library/… )
— David Schwartz

@DavidSchwartz - Đã thêm.
— WraithNath

Tôi có chính xác vấn đề thiết kế giống như bạn, đây có vẻ là một giải pháp tốt. Vâng, điều này hoặc tôi loại bỏ databinding và điều khiển thủ công ui thông qua mã phía sau trong chế độ xem của tôi. Bạn có thể thực hiện hai cách ràng buộc với cách tiếp cận này không?
— cuộn

@rolls vâng, bạn có thể, chỉ cần đảm bảo rằng bộ mô tả thuộc tính của bạn không trả lại rằng nó chỉ đọc. Tôi đã sử dụng một cách tiếp cận tương tự gần đây cho cái gì khác quá mà chương trình dữ liệu trong một TreeList cho phép dữ liệu được chỉnh sửa trong các tế bào
— WraithNath

1

Bạn nên xem xét DependencyObjects được WPF sử dụng, chúng tuân theo một mẫu tương tự, theo đó các thuộc tính có thể được chỉ định trong thời gian chạy. Như đã đề cập ở trên, điều này cuối cùng hướng tới việc sử dụng bảng băm.

Một điều hữu ích khác cần xem là CSLA.Net . Mã này có sẵn miễn phí và sử dụng một số nguyên tắc \ mẫu mà nó có vẻ như bạn đang theo đuổi.

Ngoài ra, nếu bạn đang xem xét sắp xếp và lọc, tôi đoán bạn sẽ sử dụng một số loại lưới. Một giao diện hữu ích để triển khai là ICustomTypeDescriptor, điều này cho phép bạn ghi đè hiệu quả những gì xảy ra khi đối tượng của bạn được phản chiếu để bạn có thể trỏ bộ phản xạ đến bảng băm bên trong của đối tượng.


1

Để thay thế cho một số mã của orsogufo, vì gần đây tôi đã tự mình sử dụng từ điển cho vấn đề tương tự, đây là toán tử [] của tôi:

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

Với việc triển khai này, bộ cài đặt sẽ thêm các cặp khóa-giá trị mới khi bạn sử dụng []=nếu chúng chưa tồn tại trong từ điển.

Ngoài ra, đối với tôi propertieslà một IDictionaryvà trong các hàm tạo, tôi khởi tạo nó new SortedDictionary<string, string>().


Tôi đang thử giải pháp của bạn. tôi đang thiết lập một giá trị ở bên cạnh dịch vụ như record[name_column] = DBConvert.To<string>(r[name_column]);nơi recordlà DTO của tôi. Làm cách nào để nhận được giá trị này ở phía khách hàng?
— Rohaan

1

Tôi không chắc lý do của bạn là gì, và ngay cả khi bạn có thể giải quyết vấn đề đó bằng cách nào đó với Reflection Emit (Tôi không chắc bạn có thể làm được), thì đó không phải là một ý kiến ​​hay. Ý tưởng tốt hơn có lẽ là có một số loại Từ điển và bạn có thể truy cập vào từ điển thông qua các phương thức trong lớp của mình. Bằng cách đó, bạn có thể lưu trữ dữ liệu từ cơ sở dữ liệu trong từ điển này, rồi truy xuất chúng bằng các phương pháp đó.


0

Tại sao không sử dụng một trình lập chỉ mục với tên thuộc tính là một giá trị chuỗi được truyền cho trình chỉ mục?


0

Bạn không thể chỉ để lớp của bạn hiển thị một đối tượng Từ điển? Thay vì "đính kèm thêm thuộc tính vào đối tượng", bạn có thể chỉ cần chèn dữ liệu của mình (với một số định danh) vào từ điển tại thời điểm chạy.


0

Nếu nó là để ràng buộc, thì bạn có thể tham khảo các chỉ mục từ XAML

Text="{Binding [FullName]}"

Ở đây nó đang tham chiếu tới trình chỉ mục lớp với khóa "FullName"

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookie và Chính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.