Attribute with action/condition
Asked Answered
A

3

6

Is it possible to specify achieve following:

[SomeAttribute(condition1)]
public SomeType SomeSetting1 {get; set;}

[SomeAttribute(condition2)]
public SomeType SomeSetting2 {get; set;}

where condition is something complicated? To example,

[SomeAttribute(SomeSetting3 == 4 && SomeSetting4 < 100)]

I am using PropertyGrid to show/edit configuration as properties of some serializable class. And I need to have sort of cascading: when some setting is set, some others may be hidden, depending on the value.

Currently, I can do hide some setting in this way:

  • create new attribute, based on IHide
  • assign it to needed properties
  • check all attributes for a given property in ConfigWrapper, if there is any of IHide type, then check its Hide to decide whenever show (add to result collection of properties) or not.

    public interface IHide
    {
        bool Hide { get; }
    }
    
    public class AdminAttribute : Attribute, Common.IHide
    {
        public bool Hide
        {
            get { return !MySettings.Admin; }
        }
    
        public override object TypeId { get { return "AdminAttributeId"; } }
    }
    
    // admin only setting
    [Admin]
    public SomeType SomeSetting {get; set;}
    

This way I have to add a new attribute for any new setting (which have to hide some other settings) or combinations (and this is why I want something more generic). Of course sometimes I can use attribute parameter, to be able to use one attribute for several similar purposes:

public class ElementAttribute : Attribute, Common.IHide
{
    private string _element;
    public bool Hide
    {
        get { return !Something.Instance.IsElement(_element); }
    }

    public ElementAttribute(string element)
    {
        _element = element;
    }

    public override object TypeId { get { return "ElementAttributeId"; } }
}

By using this attribute, I can specify element symbol:

 // setting will be shown if element a present
 [Element('a')]
 public SomeType SomeSetting {get; set;}

After creating multiple of such, I came to idea what maybe it is possible somehow to code that Hide() method condition into the attribute parameter itself??? Or perhaps specify behavior (action) somehow?

I can pretty easily do it by using CodeDom me think, but it would be very sloooooow. It is possible to enumerate all attributes and cache conditions. But maybe there is an easier/alternative way? Any other ideas?

Starting bounty

I am searching for ideas to combine multiple IHide attributes (AdminAttribute - show settings when user is admin, ElementAttribute - show setting when specified element presents, etc.) into a single super-attribute. I want to be able to specify conditions somehow, without the need to create new IHide based attribute for every case.

What would be your solution if you have to handle hundreds of settings (which are existing at once), but with relations between each other and additionally related on some other conditions? How to create Admin and Element attribute behavior without creating AdminAttribute and ElementAttribute?

The point is, what there are multiple different configurations (inherited from base configuration) and I want to be able in some of them freely specify visibility conditions with a part of code, which, if evaluates into false, will hide the setting, without creating dozens of IHide based attributes. Sort of declarative programming when defining the setting itself!

Arthritis answered 20/2, 2014 at 8:59 Comment(3)
I think this SO question might help you in right direction.Orcinol
@Junaith, Thanks, but it's more like runtime conditional properties visibility, which I already have implemented with the use of wrapper (wrapper is ICustomTypeDescriptor which creates a copy of only needed properties for PropertyGrid). What I need now is conditional attribute to control properties visibility. Something what can be easily used as declarative attribute for hundreds of properties.Arthritis
AFAIK, you can't do that in compile time. You can't conditionally apply attributes based on run-time values.Orcinol
A
1

As @Jon already pointed out, using attributes is not the right way.

In my case, most satisfying solution so far is to declare another property with suffix Hide, which contains condition check code and is used via reflection in the ConfigWrapper to check whenever to add this setting or not:

public SomeType SomeSetting1 { get; set; }

public SomeType SomeSetting2 { get; set; }
protected SomeType SomeSetting2Hide { get { return SomeSetting3 = 4 && SomeSettings4 < 100; } }

This setting has to be declared as protected (at first I made a stupid mistake), to hide it itself from public settings.

And then in config wrapper:

    public ConfigWrapper(object obj)
    {
        _original = obj;
        // copy all properites
        foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(obj))
        {
            // filter hideable attributes
            bool add = true;
            foreach (Attribute attribute in property.Attributes)
                if (attribute is Common.IHide && (attribute as Common.IHide).Hide)
                {
                    add = false;
                    break;
                }

            ////////////////////////

            // filter configurable via hide property properties
            var hide = obj.GetType().GetProperty(property.Name + "Hide", BindingFlags.Instance | BindingFlags.NonPublic);
            if (hide != null && (bool)hide.GetValue(obj, null))
                add = false;

            ///////////////////////

            // add
            if (add)
                _collection.Add(new ConfigDescriptor(property));
        }
    }
Arthritis answered 10/3, 2014 at 6:45 Comment(1)
This is how the XmlSerializer works btw. I am not sure if that applies to all properties but I think so. When the XmlSerializer encounters a property it checks for a property with the same name and the postfix "Specified". If that property returns false the XmlSerializer will in fact ignore the property. I figured this out when using the xsd code generator that generates such "Specified" properties for nullable structs.Schematism
B
9

That's not a good idea

It will be unreasonably difficult to do something like what you describe, and the result would not be maintainable even if you did manage to do it.

The difficulty stems from the restrictions on attribute parameters:

Attribute parameters are restricted to constant values of the following types:

  • Scalar types (bool, byte, char, short, int, long, float, and double)
  • string
  • System.Type
  • enums
  • object (which must be a constant value of one of the above types)
  • One-dimensional arrays of any of the above types

It's obvious that the only way you could squeeze a predicate into any of the above types is by writing a string a-la SQL, e.g.

[Hide("foo = \"42\" && !bar")]
public object MyProperty { get; set; }

You would then need to parse this string at runtime, convert it to a machine-usable form and decide what the result would be. And even then it would be very easy to write an invalid predicate since the string is totally opaque to the compiler.

But there are alternatives

Your attempted solution is really trying to swim against the current -- attributes were not meant to encapsulate runtime behavior. Instead of doing this, why not simply have your serializable class implement a suitable interface? For example, you could start with the bog-standard

public interface IConditionalPropertySource
{
    bool IsPropertyApplicable(string propertyName);
}

class Test : IConditionalPropertySource
{
    public string SomeSetting { get; set; }

    public bool IsPropertyApplicable(string propertyName)
    {
        switch (propertyName)
        {
            case "SomeSetting":return DateTime.Now.DayOfWeek == DayOfWeek.Friday;
            default: return false;
        }
    }
}

This will do the job but it does have some drawbacks:

  1. Property names are not checked by the compiler; both the caller and the implementation of IsPropertyApplicable could make mistakes (e.g. simple misspelling) which would not be flagged.
  2. It's not immediately clear which properties are conditional and which are not just by looking at their declarations.
  3. The exact relationship between properties and conditions is somewhat hidden.

With compile-time safety too

If the above is not satisfactory, you can improve on it by eliminating the first two issues and improving on the third one for a small runtime cost. The idea is based on a well-known trick to provide compile-time safety when referring to property names: instead of specifying them as a string, specify them as member access expressions.

public interface IConditionalPropertySource<T>
{
    bool IsPropertyApplicable(Expression<Func<T, object>> expr);
}

You can call the above as IsPropertyApplicable(o => o.SomeSetting) and get "SomeSetting" as a string at runtime with ((MemberExpression)expr.Body).Member.Name. However, we don't really want to work with a bare string at any time because that would mean issue #1 above still exists.

So instead of this, we can create a dictionary that maps member access expressions to boolean functions and provide an equality comparer that replaces the default equality semantics for expressions (reference equality) with member name equality:

class Test : IConditionalPropertySource<Test>
{
    // Your properties here:
    public string SomeSetting { get; set; }

    // This is the equality comparer used for the dictionary below
    private class MemberNameComparer :
        IEqualityComparer<Expression<Func<Test, object>>>
    {
        public bool Equals(
            Expression<Func<Test, object>> lhs, 
            Expression<Func<Test, object>> rhs)
        {
            return GetMemberName(lhs).Equals(GetMemberName(rhs));
        }

        public int GetHashCode(Expression<Func<Test, object>> expr)
        {
            return GetMemberName(expr).GetHashCode();
        }

        private string GetMemberName(Expression<Func<Test, object>> expr)
        {
            return ((MemberExpression)expr.Body).Member.Name;
        }
    }

    // A dictionary that maps member access expressions to boolean functions
    private readonly IDictionary<Expression<Func<Test, object>>, Func<bool>> 
        conditions = new Dictionary<Expression<Func<Test, object>>, Func<bool>>
        (new MemberNameComparer())
        {
            // The "SomeSetting" property is only visible on Wednesdays
            { 
                self => self.SomeSetting, 
                () => DateTime.Now.DayOfWeek == DayOfWeek.Wednesday
            }
        };


    // This implementation is now trivial
    public bool IsPropertyApplicable(Expression<Func<Test, object>> expr)
    {
        return conditions[expr]();
    }
}

This eliminates issue #1 (you can no longer misspell property names, the compiler will catch that) and it improves on #3 (properties and conditions are somewhat more visible). It still leaves issue #2 unaddressed: you can't tell if SomeProperty is conditionally visible just by looking at its declaration.

However, you could expand the code to enforce this at runtime:

  • Decorate conditionally visible properties with a custom attribute
  • Inside the constructor, enumerate all properties of the class decorated with that attribute and all property names that can be derived from the dictionary keys
  • Treat both enumerated collections as sets
  • If the sets are not equal there is a mismatch between the properties that have been decorated and those that have conditional visibility logic defined; throw an exception
Babysitter answered 5/3, 2014 at 10:11 Comment(3)
Yes, attributes with strings aren't good, that was also my first idea. Parsing isn't a problem (I could utilize CodeDom), but compile-time check and maintainability is awkward. Your idea with method to check conditions actually give me an idea too. What if I create a method called SomeSetting1Hide() (or rather another, private property with only getter???) to embed condition into? I could declare them together, so it will be pretty well visible! And Expression part with dictionaries looks to me too much centralized definition (I'll have to look for it every time when changing something).Arthritis
What I mean is to have properties like bool SomeSetting1Hide { get { return SomeProperty2 > 100 && SomeProperty3 == 0; } }, which will be invoked before checking if SomeSetting1 have to be added to a properties list, shown in PropertyGrid. Can you perhaps elaborate this idea further? =PArthritis
@Sinatr: Well, you could take the basic version (with string argument) and simply make it find and call the Hide method with reflection, e.g. return (bool)this.GetType().GetMethod(propertyName + "Hide").Invoke(this, null). But I don't really like this.Babysitter
A
1

As @Jon already pointed out, using attributes is not the right way.

In my case, most satisfying solution so far is to declare another property with suffix Hide, which contains condition check code and is used via reflection in the ConfigWrapper to check whenever to add this setting or not:

public SomeType SomeSetting1 { get; set; }

public SomeType SomeSetting2 { get; set; }
protected SomeType SomeSetting2Hide { get { return SomeSetting3 = 4 && SomeSettings4 < 100; } }

This setting has to be declared as protected (at first I made a stupid mistake), to hide it itself from public settings.

And then in config wrapper:

    public ConfigWrapper(object obj)
    {
        _original = obj;
        // copy all properites
        foreach (PropertyDescriptor property in TypeDescriptor.GetProperties(obj))
        {
            // filter hideable attributes
            bool add = true;
            foreach (Attribute attribute in property.Attributes)
                if (attribute is Common.IHide && (attribute as Common.IHide).Hide)
                {
                    add = false;
                    break;
                }

            ////////////////////////

            // filter configurable via hide property properties
            var hide = obj.GetType().GetProperty(property.Name + "Hide", BindingFlags.Instance | BindingFlags.NonPublic);
            if (hide != null && (bool)hide.GetValue(obj, null))
                add = false;

            ///////////////////////

            // add
            if (add)
                _collection.Add(new ConfigDescriptor(property));
        }
    }
Arthritis answered 10/3, 2014 at 6:45 Comment(1)
This is how the XmlSerializer works btw. I am not sure if that applies to all properties but I think so. When the XmlSerializer encounters a property it checks for a property with the same name and the postfix "Specified". If that property returns false the XmlSerializer will in fact ignore the property. I figured this out when using the xsd code generator that generates such "Specified" properties for nullable structs.Schematism
S
0

You can look at this framework:

http://www.codeproject.com/Articles/415070/Dynamic-Type-Description-Framework-for-PropertyGri

This has many dynamic featuers.

Suellen answered 20/6, 2014 at 14:23 Comment(2)
Please don't post link-only answersChanachance
Welcome to Stack Overflow! Link-only answers, while they may provide an answer to the question, can become invalid if the page changes: please provide a more substantial answer with information from your link.Wellfavored

© 2022 - 2024 — McMap. All rights reserved.