aloneguid / config

⚙ Config.Net - the easiest configuration framework for .NET developers. No BS.
MIT License
656 stars 86 forks source link

DescriptionAttribute and CategoryAttribute for Property Grid #107

Closed bepursuant closed 3 years ago

bepursuant commented 4 years ago

I've taken to throwing a property grid in some of my projects to easily edit configuration values. It's not the most flexible, but it works GREAT, and the way this library auto saves values means that selecting the config proxy in a property grid implements a full config UI with zero effort. Beautiful!

But I would love to add Descriptions and Categories to my config properties, so that they group together and I can provide notes about what the properties control.

I tried to attach a DescriptionAttribute from System.ComponentModel to the Config interface, but I believe it is being lost due to how attributes work, and also possibly due to how Castle is generating the proxy.

Is there a way to retain attributes, or to tell Castle to retain attributes from the interface definition?

Minimal example demonstrating a Description and Category attribute, and that they are not seen by the property grid when the config is the SelectedObject.

using Config.Net;
using System.ComponentModel;
using System.Windows.Forms;

namespace ConfigProperties
{
    public interface ITestConfig
    {
        [DefaultValue("A Property Value!"), 
         Description("A highly configurable item!"),
         Category("Main Category")]
        string SomeProperty { get; set; }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            // config proxy
            ITestConfig cfg = new ConfigurationBuilder<ITestConfig>().Build();

            // property grid
            PropertyGrid propertyGrid1 = new PropertyGrid
            {
                Dock = DockStyle.Fill,
                Location = new System.Drawing.Point(0, 0),
                SelectedObject = cfg
            };

            // window
            AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new System.Drawing.Size(350, 200);
            Controls.Add(propertyGrid1);
            Text = "ConfigProperties";
        }
    }
}

image

tekook commented 3 years ago

I don't think the proxy keeps and attributes. I'm currently working on validating the configuration via DataAnnotation. The resulting Proxy for the interface is missing all the attributes.

using System.ComponentModel.DataAnnotations;

namespace Tests
{
    public interface MyConfig
    {
        Processor Processor { get; set; }

        [Required, MinLength(3)]
        string UserDomain { get; set; }

        string UserName { get; set; }
    }

    public interface Processor
    {
        public int Level { get; set; }
    }
}

Edit: But come to think of it. I think this is default behavior for all interface implementations. -> https://stackoverflow.com/a/540785/1699503

bepursuant commented 3 years ago

@tekook I'll admit, some of this is just beyond my grasp as a developer. It sounds like the core C# language doesn't support Attribute inheritance from Interfaces, but clearly Config.Net is able to see attributes on the interface, as the "DefaultValue" attribute is supported and respected.

It seems like we can see Attributes for interface members, but they don't carry over to an implementation class? That's where I get a bit lost in exactly how Castle works - surely there has to be a way to tell Castle to do this, right? Or is that wishful thinking? :(

tekook commented 3 years ago

@bepursuant you are absolutly correct. Attributes defined on interfaces will not get derrived to other classes or interfaces.

Config.Net could look at the original interface, list the attributes and apply them via reflection to the generated class of Castle. But I think this should be done at Castle, but I don't know how.

bepursuant commented 3 years ago

@stakx kindly provided details that resolved this issue, and I wanted to close the issue with his explanation.

The key is to define a custom Attribute type that inherits from the base DescriptionAttribute and CategoryAttribute, but defines them as non-inherited. When these non-inherited attributes are applied to a property in a Config.Net interface, they are seen as Description and Category Attributes to the Property grid and yield the desired effect.

Full Code:

using Config.Net;
using System;
using System.ComponentModel;
using System.Windows.Forms;

namespace ConfigProperties
{
    public interface ITestConfig
    {
        [Option(DefaultValue = "A Property Value!"),
         ConfigDescription("A highly configurable item!"),
         ConfigCategory("Main Category")]
        string SomeProperty { get; set; }
    }

    [AttributeUsage(AttributeTargets.All, Inherited = false)]
    public class ConfigDescriptionAttribute : DescriptionAttribute
    {
        public ConfigDescriptionAttribute(string description) : base(description) { }
    }

    [AttributeUsage(AttributeTargets.All, Inherited = false)]
    public class ConfigCategoryAttribute : CategoryAttribute
    {
        public ConfigCategoryAttribute(string category) : base(category) { }
    }

    public partial class Form1 : Form
    {
        public Form1()
        {
            // config proxy
            ITestConfig cfg = new ConfigurationBuilder<ITestConfig>().Build();

            // property grid
            PropertyGrid propertyGrid1 = new PropertyGrid
            {
                Dock = DockStyle.Fill,
                Location = new System.Drawing.Point(0, 0),
                SelectedObject = cfg
            };

            // window
            AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new System.Drawing.Size(350, 200);
            Controls.Add(propertyGrid1);
            Text = "ConfigProperties";
        }
    }
}

Yields (note the "Main Category" and the Description shown in the help box at the bottom): image

bepursuant commented 3 years ago

The resulting Proxy for the interface is missing all the attributes.

@tekook - take a look at the resolution above. I think you can create similar custom attributes that simply extend RequiredAttribute and MinLengthAttribute with Inherited=false and get the behavior you're looking for :)

stakx commented 3 years ago

@bepursuant @tekook, I'll mention it for completeness' sake, though I haven't tried any of it as my Windows Forms skills have become a little rusty. There's possibly another solution that might integrate better with PropertyGrid: customizing proxies' object metadata through TypeDescriptor. IIRC, PropertyGrid uses it to get object metadata, so it might be possible to customize the lookup process for the [Description] and [Category] (and other) attributes. If so, you could keep using the standard attribute types.