属性与动作/条件

6

有没有可能指定实现以下内容:

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

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

当 "condition" 是某些复杂的东西时,该怎么办?例如:

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

我正在使用 PropertyGrid 来显示/编辑配置作为可序列化类的属性。而且我需要有一种级联的方式:当设置某个值时,其他一些设置可能会被隐藏,这取决于该值。

目前,我可以通过以下方式隐藏某些设置:

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

这样,我必须为任何新的设置(需要隐藏其他设置)或组合添加一个新属性(这就是我希望有更多通用东西的原因)。当然,有时我可以使用属性参数,以便能够将一个属性用于几个类似的目的:

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

通过使用这个属性,我可以指定元素的符号:

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

创建了多个这样的对象后,我的想法是或许能够将Hide()方法的条件编码到属性参数中??? 或者以某种方式指定行为(动作)?使用CodeDom,我认为可以很容易地实现它,但速度会非常慢。可以枚举所有属性并缓存条件。但也许有更简单/替代的方法?还有其他的想法吗?
我正在寻找一种将多个IHide属性(AdminAttribute-当用户为管理员时显示设置,ElementAttribute-当指定元素存在时显示设置等)组合成单个超级属性的方法。我希望能够以某种方式指定条件,而无需为每种情况创建新的基于IHide的属性。
如果您必须处理数百个同时存在的设置,并且它们之间存在关系并且与其他条件相关联,那么您的解决方案将是什么?如何创建与Admin和Element属性行为相同的属性,而无需创建AdminAttribute和ElementAttribute?
问题在于,存在多个不同的配置(从基本配置继承), 我希望能够在其中某些配置中自由指定可见性条件,这些条件将隐藏设置,而无需创建数十个基于IHide的属性。这是一种声明式编程,在定义设置本身时实现!

我认为这个 Stack Overflow 的问题可能会帮助你朝着正确的方向。链接 - Junaith
@Junaith,谢谢,但更像是运行时条件属性可见性,我已经使用包装器(包装器是ICustomTypeDescriptor,它仅为“PropertyGrid”创建所需属性的副本)实现了这一点。现在我需要的是条件属性来控制属性的可见性。类似于易于用作数百个属性的声明性属性的东西。 - Sinatr
据我所知,你不能在编译时做到这一点。你不能根据运行时的值有条件地应用属性。 - Junaith
3个回答

9

这不是一个好主意

像你描述的那样做某事将会非常困难,并且即使你设法完成了,结果也无法维护。

困难在于属性参数的限制:

属性参数仅限于以下类型的常量值:

  • 标量类型(bool、byte、char、short、int、long、float 和 double)
  • string
  • System.Type
  • 枚举
  • object(必须是上述类型之一的常量值)
  • 任何上述类型的一维数组

显然,你唯一能够将谓词挤入以上任何类型的方式就是写一个类似 SQL 的字符串,例如:

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

您需要在运行时解析此字符串,将其转换为机器可用的形式并决定结果是什么。即使这样,编写无效谓词也非常容易,因为编译器对该字符串完全不透明。
但是还有其他选择。
您尝试的解决方案实际上是在逆流而上 - 属性并不意味着封装运行时行为。与其这样做,为什么不让您的可序列化类实现适当的接口呢?例如,您可以从标准模板开始。
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;
        }
    }
}

这样做可以完成任务,但它确实有一些缺点:
  1. 属性名称未经编译器检查;调用者和IsPropertyApplicable的实现都可能出错(例如简单的拼写错误),但不会被标记。
  2. 仅通过查看声明,无法立即清楚哪些属性是有条件的,哪些属性不是。
  3. 属性与条件之间的确切关系有些隐藏。

同时具备编译时安全性

如果以上方法不令人满意,您可以通过消除前两个问题并在小的运行时成本上改进第三个问题来改善它。这种想法基于提供属性名称时提供编译时安全性的众所周知的技巧:不要将它们指定为字符串,而是指定为成员访问表达式。

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

您可以调用上述方法:IsPropertyApplicable(o => o.SomeSetting) 并在运行时使用 ((MemberExpression)expr.Body).Member.Name 作为字符串获取 "SomeSetting"。但是,我们实际上不想在任何时候都使用裸字符串,因为这意味着问题#1仍然存在。
因此,我们可以创建一个字典,将成员访问表达式映射到布尔函数,并提供一个相等比较器,用成员名称相等替换表达式的默认相等语义(引用相等)。
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]();
    }
}

这样做消除了问题 #1(您不再会拼错属性名称,编译器会捕获它),并改善了问题 #3(属性和条件更加明显)。但它仍然没有解决问题 #2:您无法仅通过查看其声明就知道 SomeProperty 是否有条件可见性。
然而,您可以扩展代码以在运行时执行此操作:
  • 使用自定义属性装饰有条件可见性的属性
  • 在构造函数中,枚举具有该属性装饰的类的所有属性以及可以从字典键派生的所有属性名称
  • 将两个枚举集合都视为集合
  • 如果集合不相等,则表示已经装饰的属性与定义有条件可见性逻辑的属性存在不匹配;抛出异常

是的,具有字符串的属性不好,那也是我的第一个想法。解析不是问题(我可以利用CodeDom),但编译时检查和可维护性很尴尬。你的条件检查方法的想法其实给了我另一个想法。如果我创建一个名为SomeSetting1Hide()的方法(或者更准确地说,只有getter的私有属性?),把条件嵌入其中怎么样?我可以一起声明它们,这样会非常明显!而且,使用字典的Expression部分对我来说也太过于集中定义(每次更改内容时都要寻找它)。 - Sinatr
我的意思是拥有像bool SomeSetting1Hide { get { return SomeProperty2 > 100 && SomeProperty3 == 0; } }这样的属性,它将在检查是否必须将SomeSetting1添加到属性列表并显示在PropertyGrid之前被调用。你能否进一步阐述这个想法呢?=P - Sinatr
@Sinatr:嗯,你可以使用基本版本(带有字符串参数),然后通过反射查找并调用“Hide”方法,例如return (bool)this.GetType().GetMethod(propertyName + "Hide").Invoke(this, null)。但我不太喜欢这种方法。 - Jon

1

正如@Jon所指出的那样,使用属性并不是正确的方法。

在我的情况下,迄今为止最令人满意的解决方案是声明另一个带有后缀Hide的属性,其中包含条件检查代码,并通过反射在ConfigWrapper中使用以检查是否添加此设置:

public SomeType SomeSetting1 { get; set; }

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

这个设置必须声明为protected(起初我犯了一个愚蠢的错误),以将其本身隐藏在公共设置之外。

然后在配置包装器中:

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

这就是 XmlSerializer 的工作原理。我不确定是否适用于所有属性,但我认为是这样的。当 XmlSerializer 遇到一个属性时,它会检查是否存在一个同名且后缀为“Specified”的属性。如果该属性返回 false,则 XmlSerializer 实际上会忽略该属性。我发现这一点是在使用 xsd 代码生成器时,该生成器为可空结构生成了这样的“Specified”属性。 - Noel Widmer

0

2
请勿发布仅包含链接的答案。 - Phantômaxx
欢迎来到 Stack Overflow!仅提供链接的答案,虽然可能回答了问题,但如果页面更改,它们可能会变得无效:请提供更实质性的答案,并附带来自您链接的信息。 - AstroCB

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接